Какие утверждения верны относительно определения множественного наследования
Перейти к содержимому

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

Урок №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 после имени класса.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *