Версия для печати темы
Нажмите сюда для просмотра этой темы в оригинальном формате
Форум программистов > C/C++: Общие вопросы > Наследование:его смысл на небольшой задаче


Автор: ЛунныйОборотень 26.11.2012, 15:03
Здравствуйте!
Читаю книгу Страуструпа по С++,главу про наследование.
И не могу понять,зачем это нужно?

В чем преимущество,по сравнению с обычным копипастом?
Меньше кода?

Еще вопрос: что в базовом классе лучше выносить на публичный доступ и что на закрытый?

Например,QTcpSocket?


Спасибо.Буду признателен за объяснение.

Автор: Arantir 26.11.2012, 20:12
Цитата

Наследование:его смысл на небольшой задаче

Очень правильный вопрос. В небольшой задаче от него зачастую совершенно никакой пользы.
Суть его раскрывается на более крупных проектах.
Играли когда-нибудь в Lineage 2? В сети в свободном доступе есть исходники любительских сборок серверов на Java. Если хотите, можете просто на досуге покопаться в их исходниках. Копаться в открытых исходниках любительских сборок - это законно =) Сразу увидите, зачем нужны такие вещи, как интерфейсы, абстрактные классы, наследование...

Объясняю я не очень хорошо, но попробую.
Для чего-то некрупного в голову приходит пару случаев, в которых полезно наследование:
1. Логически объединить некие сущности в классы, где родительский класс является обобщением, а дочерний - частным случаем.
Код

класс Многоуголник
{
    количество углов;
    массив длин ребер;
    площадь()
    {
        какая-то жуть-замутная формула =)
    }
}

класс Прямоугольник : Многоугольник
{
    площадь()
    {
        первая сторона *  вторая сторона // ширина на высоту
    }
}
класс Квадрат : Прямоугольник
{
    
    площадь()
    {
        первая сторона ^ 2 // длина стороны, возведенная в 2 степень
    }
}
Итого такие свойства, как количество углов и массив ребер мы содержим в базовом классе, а в дочерних - всего-лишь формулы, например, требующие от компьютера меньше действий/вычислений в каждом частном случае.

2. Наследование от интерфейсов. Гарантирует, что программисту придется написать все требуемые методы какого-то класса (который вы, например, взяли у коллеги). То есть программа не будет вылетать в экран смерти из-за недоглядки =)
Код

интерфейс Сравниваемое
{
    сравнить(Сравниваемое что-то)
    {
        если это > что-то 
            1
       если же  это < что-то 
            -1
       в конце-концов
           0
    }
}

класс Сортировщик
{  
    отсортировать(массив Сравниваемоего)
    {
        если это.сравнить(что-то) < 0
            передвинуть это выше
        в противном случае
           передвинуть это вниз
    }
}

Таким образом мы гарантируем, что сортировщик будет сортировать только то, что можно сравнить (у чего обязательно определена функция сравнения). иначе же мы получаем ошибку компиляции проги.

3. Расширение имеющихся классов, которые мы не можем изменять непосредственно.
Код

класс Системный
{
    сделать1() {}
    сделать2() {}
    сделать3() {}
}

класс ЭкстраСистемный : Системный
{
    сделать3()
    {
        базовый.сделать3();
        и_еще_сделать_что-то_мое();
    }
}


Ну как-то так. Но всех тонкостей сходу не объяснишь =)

Автор: ЛунныйОборотень 27.11.2012, 09:32
Хм,все таки попробую:) а то так и не вкушу наследование:))

Еще такой вопрос- вот у меня есть сокет в базовом классе,во всех производных классах он тоже нужен.
В каждом классе коннект к разным адресам.
Нужно ли его выносить в базовый класс,а в производных можно переопределить на нужный адрес?
Или в каждом классе завести свой сокет?

Я к тому,что может ли наследование сокета вызвать логическую ошибку?

Например:

Код

Базовый сокет= 192.169.1.1

Тот_же_сокет_в_проиводном_классе=192.169.1.2



Можно ли так делать?

Автор: volatile 27.11.2012, 23:59
Цитата(ЛунныйОборотень @  27.11.2012,  09:32 Найти цитируемый пост)
Например:
Базовый сокет= 192.169.1.1
Тот_же_сокет_в_проиводном_классе=192.169.1.2
Можно ли так делать? 


Здесь наследование не причём.  
Всё будет работать и без него. (если правильно код написан конечно)

1_объект_класса_Базовый_сокет = 192.169.1.1
2_объект_класса_Базовый_сокет = 192.169.1.2
 smile 


Автор: Arantir 28.11.2012, 01:10
Цитата

В каждом классе коннект к разным адресам.
Нужно ли его выносить в базовый класс,а в производных можно переопределить на нужный адрес?
Или в каждом классе завести свой сокет?

Между первым и вторым вариантами нет никакой разницы... 
Вы немного неправильно понимаете суть наследования. Наследование - это всего-лишь инструмент. Такая себе "удобность" для программиста (как и все остальные преимущества ООП). Если постараться, можно что угодно и без ООП сделать, так вот чисто вручную. И наоборот, ООП можно добавить даже в языки, на него не ориентированные, например, в JavaScripts, тоже вручную. То, что оно в С++ вшито сразу в компилятор - это и есть "удобность".
А так, ничто не ограничивает ваше воображение =) Будущее за агентно-ориентированным программированием. Языков для него еще нет, но использовать его концепции на любом языке ничто не мешает.

Наследованием полей - это, грубо говоря, автоматическое дописывание этих полей всем дочерним классам (на самом деле просто идет обращение к родительскому полю, но сути это не меняет).

Если в каждом классе значение поля гарантированно отличается от значения этого поля базового класса, то писать его в базовом классе просто-таки нет смысла. 
В любом случае при создании подключения вы должны будете использовать какой-то сокет. Думаю, при этом сразу заметите его отсутсвие, если в функцию подключения будет нечего вписать =) Как минимум компилятор выдаст вам ошибку о неинициализированной переменной.
А если вы впишите его в базовый и потом забудете переопределить в дочернем, то дочерний класс может абсолютно незаметно для вас подсоединяться не к тому что надо сокету, а к тому, который он взял у базового класса.

Автор: baldina 28.11.2012, 10:07
Цитата(ЛунныйОборотень @  27.11.2012,  09:32 Найти цитируемый пост)
Нужно ли его выносить в базовый класс,а в производных можно переопределить на нужный адрес?
Или в каждом классе завести свой сокет?


Цитата(Arantir @  28.11.2012,  01:10 Найти цитируемый пост)
Между первым и вторым вариантами нет никакой разницы... 

разница есть, причем принципиальная. второй вариант это как раз копипаст, а не наследование.

ЛунныйОборотень, если сокет это неотъемлемое свойство всех подклассов базового, значит это свойство базового класса.
В этом суть наследования: базовый класс объединяет в себе все общие признаки.
Ваш пример будет выглядеть так:
Код

class Service {
  private:
    Socket s;
  public:
    Service (const string& ip) : s(ip) {}
};

class LocalService : public Service {
  public:
    LocalService () : Service ("127.0.0.1") {}
};

Цитата(ЛунныйОборотень @  27.11.2012,  09:32 Найти цитируемый пост)
Я к тому,что может ли наследование сокета вызвать логическую ошибку?

если классификация проведена правильно, то не может

Цитата(ЛунныйОборотень @  26.11.2012,  15:03 Найти цитируемый пост)
В чем преимущество,по сравнению с обычным копипастом?

даже простой отказ от копипаста достаточное основание)))
остальные преимущества заключаются в свойствах ООП, связанных с наследованием - это проверка типа (подклассы являются экземплярами суперкласса) и полиморфизм.

Цитата(ЛунныйОборотень @  26.11.2012,  15:03 Найти цитируемый пост)
Еще вопрос: что в базовом классе лучше выносить на публичный доступ и что на закрытый?

в публичный выносятся те функции, которые определяют логический интерфейс класса. а технические подробности (переменные, а также функции, связанные с реализацией) должны быть закрыты.

Автор: Arantir 28.11.2012, 10:56
Цитата(baldina @  28.11.2012,  09:07 Найти цитируемый пост)
разница есть, причем принципиальная. второй вариант это как раз копипаст, а не наследование.

Я имел ввиду результат. Принципиальная... есть такое выражение "дело принципа" =)
Мое мнение состоит в том, что ООП нужно, когда прямо чувствуется, что с ним проще и быстрее, чем без него.
А так в каждом классе, что переопределять, что определять эту переменную... Только одна практическая разница, о чем выше упоминал, что без ее определения она у родителя будет взята.

Автор: baldina 28.11.2012, 11:27
Цитата(Arantir @  28.11.2012,  10:56 Найти цитируемый пост)
Я имел ввиду результат

почему бы на ассемблере не написать? с тем же результатом...

Автор: Arantir 28.11.2012, 11:39
Примерно потому, почему пишут на Java место C++ =)

Автор: ЛунныйОборотень 28.11.2012, 13:14
Цитата(baldina @  28.11.2012,  10:07 Найти цитируемый пост)
LocalService 


Цитата(baldina @  28.11.2012,  10:07 Найти цитируемый пост)
код C++

class Service {
  private:
    Socket s;
  public:
    Service (const string& ip) : s(ip) {}
};
class LocalService : public Service {
  public:
    LocalService () : Service ("127.0.0.1") {}
};



Пожалуй,в каждом классе сделаю по сокету-чтобы не путаться.

Могу я задать следующий вопрос?
Если да, то в процессе написания сделал абстрактный класс с чисто виртуальным методом func1() 

И в производном классе теперь его пробую переопределить.Но,возможно,делаю это неправильно.

Абстрактный Класс
Код

virtual void getValule()=0;


В Производном классе 

Код


void Proizv_class::getValue(int i)
{
return i;
}

В хедере производного класса я написал прототип этой функции.
У меня закрадывается смутное подозрение,что в результате базовая функция не используется вообще,даже как чисто виртуальная.То есть это две разные функции,не связанные наследованием.

Это так? А если бы я и там и там написал бы getValue()?

Только начал вплотную заниматься наследованием,в меру сил читаю Страуструпа,но все-таки вопросы остаются.

Спасибо большое за ответы.

Автор: baldina 28.11.2012, 13:46
Цитата(ЛунныйОборотень @  28.11.2012,  13:14 Найти цитируемый пост)
У меня закрадывается смутное подозрение,что в результате базовая функция не используется вообще,даже как чисто виртуальная.

сигнатуры функций (число и тип аргументов, возвращаемого значения) должны совпадать, иначе они считаются различными функциями.
Код

// две _разные_ функции bar()
struct Foo {
  int bar(int);
  int bar(float);
};


пример с виртуальными функциями
Код

struct Shape {
  virtual void draw() = 0;
};

struct Circle : public Shape {
  void draw() { cout << "i'm circle\n"; }
};
struct Rect : public Shape {
  void draw() { cout << "i'm rectangle\n"; }
};

int main () {
  Shape *shapes[] = { new Circle, new Rect };
  for (int i=0; i < 2; ++i)
    shapes[i]->draw();
}


Цитата(ЛунныйОборотень @  28.11.2012,  13:14 Найти цитируемый пост)
Пожалуй,в каждом классе сделаю по сокету-чтобы не путаться.

зря. зачем делать лишнюю работу? если использовать только наследование (без полиморфизма), базовый класс является как-бы шаблоном для производного.
классы - для компилятора, в процессе выполнения нет никаких классов, лишь объекты. переменная типа производного класса включает в себя свои переменные и функции, а также переменные и функции базовых классов.

Автор: ЛунныйОборотень 28.11.2012, 14:20
То есть,определяя базовый класс и потом вызывая его через производный ,мы создаем непересекающиеся множества.
И если,допустим ,результатом выполнения этой функции будет какое то значение,записанное в переменную в базовом классе,

эта переменная будет как бы в параллельных мирах?))

То же самое и с сокетом?Название и определение в одном месте(базовом классе),а значения и параметры этого сокета будут разными для каждого класса?

Автор: Arantir 28.11.2012, 14:44
ЛунныйОборотень, если переменной нет в дочернем классе, она берется из базового. Если она есть в дочернем классе - берется из дочернего. Переменная дочернего класса и переменная базового класса - это две разные переменные. Они не в параллельных мирах, они просто 2 разные переменные в оперативной памяти.
К переменной базового класса можно получить доступ (если она public/protected, конечно же) в любое время через псевдокласс parent (например, parent::some_val), который автоматически представляет родительский класс.

Непересекающиеся множества - это 2 объекта одного класса. А дочерний класс может всяко-разно взаимодействовать с базовым. Переопределенные функции тоже можно вызвать в их оригинале через parent.

Представь, что имя переменной состоит из nameespac'а, класса и самой переменной. Типа так: "programm::myclass:var". Тогда заметно, что нет никаких параллельных миров. Есть только служебные словечки, ограничивающие доступ одним объектам к другим. А так - все "рядом".

Цитата

То же самое и с сокетом?Название и определение в одном месте(базовом классе),а значения и параметры этого сокета будут разными для каждого класса? 

Нет. Если определение только в базовом, то везде будет значение базового, одинаковое. При этом определять заново переменную не обязательно.
Но если надо разные значения, то следует определить переменную с тем же именем в дочернем классе, тогда компилятор будет в первую обращаться к переменной дочернего класса.

Автор: baldina 28.11.2012, 15:06
Цитата(Arantir @  28.11.2012,  14:44 Найти цитируемый пост)
А дочерний класс может всяко-разно взаимодействовать с базовым. Переопределенные функции тоже можно вызвать в их оригинале через parent.

так ТС запутается еще больше. нет никакого взаимодействия дочернего класса с базовым. 
содержимое базового класса целиком входит в производный класс. технически они рядом, синтаксически мы в некоторых случаях должны указывать, что используется переменная или функция, определенная в базовом классе.

рассмотрим вот такую структуру данных: { 1, { 2, {3} } }. 
она может описываться, например, так:
Код

struct foo {
  int v0;
};

struct bar {
  foo f;
  int v1;
};

struct baz {
  bar b;
  int v2;
};

int main () {
  baz z;
  z.b.f.v0 = 3;
  z.b.v1 = 2; 
  z.v2 = 1; 
}


это - композиция. если по смыслу задачи baz не просто включает в себя данные bar (и foo), а является расширением понятия bar (и foo),
можно использовать наследование.

Код

struct foo {
  int v0;
};

struct bar : public foo {
  int v1;
};

struct baz : public bar {
  int v2;
};

int main () {
  baz z;
  z.v0 = 3; // foo::v0
  z.v1 = 2; // bar::v1
  z.v2 = 1; 
}

с точки зрения хранимых данных оба описания эквивалентны. использование наследования нам дает, во-первых, более короткую запись, во-вторых, возможность обращаться с объектами типа baz как с объектами типа bar и foo без явного преобразования типов.

Powered by Invision Power Board (http://www.invisionboard.com)
© Invision Power Services (http://www.invisionpower.com)