Правила программирования на Си и Си++

Наследование — это процесс добавления полей данных и методов-членов


В Си++ производный класс может рассматриваться как механизм добавления полей данных и обработчиков сообщений к существующему определению класса — к базовому классу. (Вы можете также смотреть на наследование как на средство изменения поведения объекта базового класса при получении им конкретного сообщения. Я вернусь к такой точке зрения при обсуждении виртуальных функций). В таком случае иерархия классов является просто средством представления полей данных и методов, определяемых для конкретного объекта. Объект содержит все данные и методы, объявленные на его уровне, а также на всех вышележащих уровнях.

Общая ошибка, совершаемая начинающими программистами на Си++, состоит в том, что они смотрят на иерархию классов и думают, что сообщения передаются от объектов производного класса к объектам базового класса. Помните, что иерархия классов Си++ не существует во время выполнения. Все, что у вас есть во время выполнения, это фактические объекты, чьи поля определяются во время компиляции при помощи иерархии классов.

В этом вопросе путаница создана многими книгами по языку Smalltalk, описывающими реализацию во время выполнения системы обработки сообщений так, как если бы сообщения передавались от производного к базовому классу.5 Это просто неверно (и в случае Smalltalk, и в случае Си++). Си++ использует наследование. Производный класс — это тот же базовый класс, но с несколькими добавленными полями и обработчиками сообщений. Следовательно, когда объект Си++ получает сообщение, он или обрабатывает его, или нет; он или определяет обработчик, или получает его в наследство. Если ни то, ни другое не имеет места, то сообщение просто не может быть обработано. И оно никуда не передается.

Не путайте отношение наследования с объединением. При объединении в один класс (контейнер) вложен объект другого класса (в отличие от наследования от другого класса). Объединение в действительности лучше, чем наследование, если у вас есть возможность выбора, потому что отношения сцепления между вложенным объектом и внешним миром гораздо слабее, чем отношения между базовым классом и внешним миром.


Объединение позволяет методам контейнера действовать подобно фильтру, через который передаются сообщения, предназначенные для вложенного объекта. Обработчики сообщений часто будут иметь одинаковые имена в контейнере и во вложенном объекте. Например:
class string                       // строка
{
// ...
public:
   const string operator=( const string r );
};
class numeric_string               // строка, содержащая число


{
   string str;
   // ...
public:
   const string operator=( const string r );
}
const string numeric_string::operator=( const string r )
{
   if( r.all_characters_are_digits() )  // все символы - цифры
      str = r;
   else
      throw invalid_assignment();
   return *this;
}
Это на самом деле довольно слабый пример объединения, потому что, если бы функция operator=()
была виртуальной в базовом классе, то объект numeric_string мог бы наследовать от string и заместить оператор присваивания для проверки на верное числовое значение. С другой стороны, если перегруженная операция сложения + в классе string
выполняет конкатенацию, то вам может понадобиться перегрузить + в классе numeric_string для выполнения арифметического сложения (т.е. преобразовывать строки в числа, которые складывать и затем присваивать результат строке). Объединение в последнем случае решило бы немного проблем.
Возвращаясь к наследованию, отметим, что объекты классов, показанных в таблице 3, вероятно будут размещаться в памяти одинаково. Каждое из этих определений, как вы заметили, имеет компонент some_cls, но доступ к этому компоненту требует совершенно разных процедур и механизмов. В этой книге я использую выражение "компонент базового класса" по отношению к той части объекта, которая определена на уровне базового класса, а не к вложенному объекту. То есть, когда я говорю, что объект производного класса имеет "компонент базового класса", то имею в виду, что некоторые из его полей и обработчиков сообщений определены на уровне базового класса. При рассмотрении вложенного объекта я буду называть его "полем" или "вложенным объектом".


Таблица 3. Два определения класса, одинаково представляемые на уровне машинного кода

Объединение
Наследование
class container
{
   some_cls contained;
   // ...
};
class base : public some_cls
{
// ...
};
Содержание раздела