Урок №161. Множественное наследование
До сих пор мы рассматривали только одиночные наследования, когда дочерний класс имеет только одного родителя. Однако C++ предоставляет возможность множественного наследования.
Множественное наследование
Множественное наследование позволяет одному дочернему классу иметь несколько родителей. Предположим, что мы хотим написать программу для отслеживания работы учителей. Учитель — это Human. Тем не менее, он также является Сотрудником (Employee).

Множественное наследование может быть использовано для создания класса Teacher, который будет наследовать свойства как Human, так и Employee. Для использования множественного наследования нужно просто указать через запятую тип наследования и второй родительский класс:
Проблемы с множественным наследованием
Хотя множественное наследование кажется простым расширением одиночного наследования, оно может привести к множеству проблем, которые могут заметно увеличить сложность программ и сделать кошмаром дальнейшую поддержку кода. Рассмотрим некоторые из подобных ситуаций.
Во-первых, может возникнуть неоднозначность, когда несколько родительских классов имеют метод с одним и тем же именем, например:
При компиляции c54G.getID() компилятор смотрит, есть ли у WirelessAdapter метод getID(). Этого метода у него нет, поэтому компилятор двигается по цепочке наследования вверх и смотрит, есть ли этот метод в каком-либо из родительских классов. И здесь возникает проблема — getID() есть как у USBDevice, так и у NetworkDevice. Следовательно, вызов этого метода приведет к неоднозначности и мы получим ошибку, так как компилятор не будет знать какую версию getID() ему вызывать.
Тем не менее, есть способ обойти эту проблему. Мы можем явно указать, какую версию getID() следует вызывать:
Хотя это решение довольно простое, но всё может стать намного сложнее, если наш класс будет иметь от 4 родительских классов, которые, в свою очередь, будут иметь свои родительские классы. Возможность возникновения конфликтов имен увеличивается экспоненциально с каждым добавленным родительским классом, и в каждом из таких случаев нужно будет явно указывать версии методов, которые следует вызывать, дабы избежать возможности возникновения конфликтов имен.
Во-вторых, более серьезной проблемой является «алмаз смерти» (или «алмаз обреченности»). Это ситуация, когда один класс имеет 2 родительских класса, каждый из которых, в свою очередь, наследует свойства одного и того же родительского класса. Иллюстративно мы получаем форму алмаза.
Например, рассмотрим следующие классы:
Сканеры и принтеры — это устройства, которые получают питание от розетки, поэтому они наследуют свойства PoweredDevice. Однако ксерокс (Copier) включает в себя функции как сканеров, так и принтеров.

В этом контексте возникает много проблем, включая неоднозначность при вызове методов и копирование данных PoweredDevice в класс Copier дважды. Хотя большинство из этих проблем можно решить с помощью явного указания, поддержка и обслуживание такого кода может привести к непредсказуемым временным затратам. Мы поговорим детально о способах решения проблемы «алмаза смерти» на соответствующем уроке.
Стоит ли использовать множественное наследование?
Большинство задач, решаемых с помощью множественного наследования, можно решить и с использованием одиночного наследования. Многие объектно-ориентированные языки программирования (например, Smalltalk, PHP) даже не поддерживают множественное наследование. Многие, относительно современные языки, такие как Java и C#, ограничивают классы одиночным наследованием обычных классов, но допускают множественное наследование интерфейсных классов. Суть идеи, запрещающей множественное наследование в этих языках, заключается в том, что это излишняя сложность, которая порождает больше проблем, чем удобств.
Многие опытные программисты считают, что множественное наследование в языке C++ следует избегать любой ценой из-за потенциальных проблем, которые могут возникнуть. Однако все же остается вероятность, когда множественное наследование будет лучшим решением, нежели придумывание двухуровневых «костылей».
Стоит отметить, что вы сами уже использовали классы, написанные с использованием множественного наследования, даже не подозревая об этом: такие объекты, как std::cin и std::cout библиотеки iostream, реализованы с использованием множественного наследования!
Правило: Используйте множественное наследование только в крайних случаях, когда задачу нельзя решить одиночным наследованием, либо другим альтернативным способом (без изобретения «велосипеда»).
Множественное наследование
Если порожденный класс наследует элементы одного базового класса, то такое наследование называется одиночным . Однако, возможно и множественное наследование. Множественное наследование позволяет порожденному классу наследовать элементы более, чем от одного базового класса. Синтаксис заголовков классов расширяется так, чтобы разрешить создание списка базовых классов и обозначения их уровня доступа:
Класс А обобщенно наследует элементы всех трех основных классов. 
Для доступа к членам порожденного класса, унаследованного от нескольких базовых классов, используются те же правила, что и при порождении из одного базового класса. Проблемы могут возникнуть в следующих случаях:
- если в порожденном классе используется член с таким же именем, как в одном из базовых классов;
- когда в нескольких базовых классах определены члены с одинаковыми именами.
В этих случаях необходимо использовать оператор разрешения контекста для уточнения элемента, к которому осуществляется доступ, именем базового класса.
Так как объект порожденного класса состоит из нескольких частей, унаследованных от базовых классов, то конструктор порожденного класса должен обеспечивать инициализацию всех наследуемых частей. В списке инициализации в заголовке конструктора порожденного класса должны быть перечислены конструкторы всех базовых классов. Порядок выполнения конструкторов при порождении из нескольких классов следующий:
- конструкторы базовых классов в порядке их задания;
- конструкторы членов, являющихся объектами класса;
- конструктор порожденного класса.
Деструкторы вызываются в порядке обратном вызову конструкторов.
Результат выполнения
Виртуальные базовые классы
Базовый класс может быть задан только один раз в списке порождения нового класса. Однако базовый класс может встретиться несколько раз в иерархии порождения.
Такая иерархия порождения несет двусмысленность при доступе к наследуемым членам класса X и может привести к ошибкам. В этом случае класс X будет дважды присутствовать в A . Хорошо это или плохо — зависит от решаемой задачи.
Если двойное вхождение объектов класса X в объект класса А не является допустимым, существует два выхода для разрешения такой ситуации:
-
преобразование порождения из нескольких классов в порождение из одного класса и объявление дружественных классов.
Базовый класс определяется как виртуальный заданием ключевого слова virtual в списке порождения перед именем базового класса или указанием типа наследования
class A : public Y, public Z <
int key;
public :
A( int i = 0) : Y(i + 2), Z(i + 3)
<
key = Y::key + Z::key;
>
int getkey( void ) < return (key); >
>;
int main() <
A object(4);
cout << "object.key = " << object.getkey();
cin.get();
return 0;
>
Результат выполнения
Как видим, объект класса X был создан только один раз. Значение поля key изменяется каждый раз при доступе к нему как через класс Y , так и через класс Z .
Если в строках 11 и 17 этого примера убрать слово virtual , то результат работы программы изменится следующим образом:
То есть в этом случае и объект класса Y , и объект класса Z , которые наследует объект класса A , будут содержать свою собственную копию объекта класса X .
Конструкторы и деструкторы при использовании виртуальных базовых классов выполняются в следующем порядке:
- конструкторы виртуальных базовых классов выполняются до конструкторов не виртуальных базовых классов, независимо от того, как эти классы заданы в списке порождения;
- если класс имеет несколько виртуальных базовых классов, то конструкторы этих классов вызываются в порядке объявления виртуальных базовых классов в списке порождения;
- деструкторы виртуальных базовых классов выполняются после деструкторов не виртуальных базовых классов.
При порождении с использованием виртуальных базовых классов сохраняются те же правила видимости, что и при порождении с не виртуальными классами.
Простое введение в C++. Часть 4. Множественное наследование
Множественное наследование — это одна из ключевых особенностей языка C++. Рассмотрим, когда оно может потребоваться и как его использовать.
Назначение множественного наследования
Предположим, что нам нужно нарисовать на форме логотип, который состоит из квадрата и круга. Сначала посмотрим, как это сделать на Си. Предположим, что у нас уже есть функции рисования квадрата и круга.
Далее везде, где нужно рисование логотипа, вызываем функцию Logo.
Теперь нам нужно то же самое сделать в C++. Предположим, что у нас уже есть классы рисования квадрата и круга. Как нарисовать логотип?
Можно, конечно, сделать так:
А потом вызывать везде эту пару строк, но это процедурный подход и он в C++ не приветствуется. В тех случаях, когда нам нужно получить методы разных классов, как раз и используется множественное наследование.
Использование множественного наследования
Для поддержки множественного наследования нужно при определении класса перечислить через запятую базовые классы. В примере для простоты вместо рисования фигур методы Draw просто выводят их названия.
Отличия множественного наследования
В случае единичного наследования производный класс получает доступ к публичным и защищенным элементам базового класса.

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

Ошибка неоднозначности
Если в базовых классах элементы называются одинаково, то в производном классе появляется ошибка неоднозначности. В примере выше, если написать просто
то компилятор выдаст ошибку, так непонятно метод какого класса нужно использовать. Ведь метод Draw есть в обоих классах.
Алмаз смерти
Теперь рассмотрим ситуацию, когда один производный класс имеет два базовых класса, при этом каждый из которых является производным одного и того же суперкласса. При этом в производных классах есть свойства с одинаковым именем.
Подобная ситуация получила название «алмаз смерти», так как на диаграмме классов такая ситуация выглядит как алмаз.

В рассмотренном примере добавим суперкласс Shape.
Мы сразу получаем множество проблем неоднозначности вызова. Так, например, если мы видим в классе Logo координаты (x, y) , то какие именно координаты мы имеем в виду?
Проблемы множественного наследования в крупных проектах
В конце концов если классы пишет один человек, то он сам все-таки представляет, что делать в случае неоднозначности вызова. Но если это используется в группе, то ситуация резко усложняется.
Лучше всего эту картину иллюстрирует карточный домик.

Представьте себе, что каждая карта — это класс, который разрабатывает отдельный программист. Нижние карты — это базовые классы, а все остальные — производные классы. И теперь представьте, что вам достался самый верхний этаж.
Вы написали класс, запустили программу и получили «алмаз смерти». К какому программисту вы будете обращаться?
Запрет наследования
Посмотрим на проблему и с другой стороны. Представьте, что вы год назад написали класс, который находится в середине иерархии классов. Отладили его, провели тесты и с ним все в порядке. Сейчас вы пишете другой класс, при этом у вас, как обычно, что-то не получается и сроки поджимают.
В этот момент к вам подходит коллега и говорит:
— Слушай, я унаследовал твой класс и класс Угрюмова. И получил алмаз смерти. Иди и решай с Угрюмовым проблему неоднозначности.
Как вы понимаете ответить этому программисту хочется только одно:
— А может ты не будешь наследовать мой класс?!
Для подобных случаев в стандарт C++ 11 добавлен спецификатор final, который запрещает наследовать данный класс. Для этого нужно написать ключевое слово final после имени класса.