- Множественное наследование
- Виртуальные базовые классы
- Лекция N. Множественное наследование, «дружба»
- Пример
- Конструкторы, деструкторы
- Как использовать множественное наследоание
- Пример наследования реализации:
- Пример злоупотребления:
- Пример наследования от интерфейсов:
- Другие особенности множественного наследования
- Способ обойти это ограничение:
- Можно явно вызвать виртуальный метод базового класса
- Таблица виртуальных функций и конструкторы
- Виртуальное наследование
- Как это работает
- «Дружба»
- Урок №161. Множественное наследование
- Множественное наследование
- Проблемы с множественным наследованием
- Стоит ли использовать множественное наследование?
- 17.9 – Множественное наследование
- Проблемы с множественным наследованием
- Множественное наследование – больше проблем, чем оно того стоит?
Множественное наследование
Если порожденный класс наследует элементы одного базового класса, то такое наследование называется одиночным . Однако, возможно и множественное наследование. Множественное наследование позволяет порожденному классу наследовать элементы более, чем от одного базового класса. Синтаксис заголовков классов расширяется так, чтобы разрешить создание списка базовых классов и обозначения их уровня доступа:
Класс А обобщенно наследует элементы всех трех основных классов.
Для доступа к членам порожденного класса, унаследованного от нескольких базовых классов, используются те же правила, что и при порождении из одного базового класса. Проблемы могут возникнуть в следующих случаях:
- если в порожденном классе используется член с таким же именем, как в одном из базовых классов;
- когда в нескольких базовых классах определены члены с одинаковыми именами.
В этих случаях необходимо использовать оператор разрешения контекста для уточнения элемента, к которому осуществляется доступ, именем базового класса.
Так как объект порожденного класса состоит из нескольких частей, унаследованных от базовых классов, то конструктор порожденного класса должен обеспечивать инициализацию всех наследуемых частей. В списке инициализации в заголовке конструктора порожденного класса должны быть перечислены конструкторы всех базовых классов. Порядок выполнения конструкторов при порождении из нескольких классов следующий:
- конструкторы базовых классов в порядке их задания;
- конструкторы членов, являющихся объектами класса;
- конструктор порожденного класса.
Деструкторы вызываются в порядке обратном вызову конструкторов.
Z() < cout "Z destroyed" endl; cin.get(); >;
>;
class A : public X, public Y, public Z
<
int key;
public :
A( int i = 0) : X(i + 1), Y(i + 2), Z(i + 3)
<
key = X::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 и может привести к ошибкам. В этом случае класс 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 .
Конструкторы и деструкторы при использовании виртуальных базовых классов выполняются в следующем порядке:
- конструкторы виртуальных базовых классов выполняются до конструкторов не виртуальных базовых классов, независимо от того, как эти классы заданы в списке порождения;
- если класс имеет несколько виртуальных базовых классов, то конструкторы этих классов вызываются в порядке объявления виртуальных базовых классов в списке порождения;
- деструкторы виртуальных базовых классов выполняются после деструкторов не виртуальных базовых классов.
При порождении с использованием виртуальных базовых классов сохраняются те же правила видимости, что и при порождении с не виртуальными классами.
Источник
Лекция N. Множественное наследование, «дружба»
Пример
От порядка родителей будет зависеть то, как лежат данные.
Конструкторы, деструкторы
От порядка родителей зависит порядок вызова конструкторов и деструкторов.
Как использовать множественное наследоание
В основном множественное наследование используется при наследовании от интерфейсов. Интерфейс в C++ это класс, у которого все функции виртуальные. Наследование реализации применяется очень редко.
Пример наследования реализации:
Мы видим что второй вариант с одиночным наследованием проще и логичнее.
Пример злоупотребления:
Но некоторые делают:
Как мы видим, множественное наследование порой логичнее и проще заменить на включение объекта внутрь нашего класса.
Одним из аргументов против множественного наследования является то, что в большинстве промышленных языков его нет, и там без него обходятся без особых сложностей.
Пример наследования от интерфейсов:
Наследование от нескольких интерфейсов является одним из самых оправданных случаев применения множественного наследования.
Другие особенности множественного наследования
Если в базовых классах есть метод с одинаковым именем, и мы его переопределили в наследнике, то он переопределятся сразу для всех родителей.
Способ обойти это ограничение:
Можно явно вызвать виртуальный метод базового класса
Таблица виртуальных функций и конструкторы
Во время последовательного вызова конструкторов при создании объекта класса, внутри тела каждого конструктора указатель на таблицу виртуальных функций будет установлен на тот класс, для которого вызван конструтор. Так как контекст будет изменяться при вызове конструторов, вызывать в них виртуальные функции является плохим тоном.
Виртуальное наследование
Рассмотрим следующую иерархию
При обычно наследовании мы бы получили в классе C две копии класса A. Но если семантика нашего наследования предполагает что C является A, также как B1 и B2, то мы получаем логическое противоречие такой конструкции. Также такая конструкция будет не верна, если нам нужно иметь одну копию класса A в C. Для преодоления этой ситуации в C++ было введено виртуальное наследование. Ключевое слово virtual при наследовании показывает компилятору, что класс наследник может учавствовать в ромбовидных иерархиях.
Вот как будет выглядеть код этого примера с виртуальным наследованием:
Как это работает
Компилятор добавляет в таблицу виртуальных функций класса наследника функцию, которая возвращает указатель на объект базового класса. Таким образом эти функции у B1 и B2 будут возвращать один и тот же указатель на объект A, сожержащийся в C. Реализация виртуального наследования не регламентируется, поэтому есть и другие подходы, например, основанный на отдельной таблице виртуальных классов.
«Дружба»
В С++ существует ключевое слово friend, означающее что класс или функция будут видеть все данные и методы класса, с которым они дружат. Если A друг B, то это не значит что B друг A.
Источник
Урок №161. Множественное наследование
Обновл. 4 Сен 2021 |
До сих пор мы рассматривали только одиночные наследования, когда дочерний класс имеет только одного родителя. Однако 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, реализованы с использованием множественного наследования!
Правило: Используйте множественное наследование только в крайних случаях, когда задачу нельзя решить одиночным наследованием, либо другим альтернативным способом (без изобретения «велосипеда»).
Источник
17.9 – Множественное наследование
До сих пор все представленные нами примеры наследования были одиночными, то есть каждый наследующий класс имеет одного и только одного родителя. Однако C++ предоставляет возможность множественного наследования. Множественное наследование позволяет производному классу наследовать члены более чем от одного родителя.
Допустим, мы хотим написать программу, чтобы отслеживать группу преподавателей. Преподаватель – это человек. Однако преподаватель также является сотрудником (и, если он работает на себя, является работодателем). Множественное наследование можно использовать для создания класса Teacher (преподаватель), который наследует свойства как от Person (человек), так и от Employee (сотрудник). Чтобы использовать множественное наследование, просто укажите все базовые классы (как и в одиночном наследовании), разделив их запятыми.
Рисунок 1 – Диаграмма наследования
Проблемы с множественным наследованием
Хотя множественное наследование кажется простым расширением одиночного наследования, оно создает множество проблем, которые могут заметно увеличить сложность программ и сделать их кошмаром для поддержки. Давайте посмотрим на некоторые из этих ситуаций.
Во-первых, неоднозначность может возникнуть, если несколько базовых классов содержат функцию с одним и тем же именем. Например:
Когда c54G.getID() компилируется, компилятор проверяет, содержит ли WirelessAdapter функцию с именем getID() . Ее у него нет. Затем компилятор проверяет, есть ли в каком-либо из родительских классов функция с именем getID() . Видите здесь проблему? Проблема в том, что c54G на самом деле содержит ДВЕ функции getID() : одна унаследована от USBDevice , а другая – от NetworkDevice . Следовательно, этот вызов функции неоднозначен, и вы получите ошибку компиляции, если попытаетесь скомпилировать этот код.
Однако есть способ обойти эту проблему: вы можете явно указать, какую версию вы хотели вызвать:
Хотя этот обходной путь довольно прост, вы можете увидеть, как всё может усложниться, когда ваш класс наследуется от четырех или шести базовых классов, которые сами наследуются от других классов. По мере того, как вы наследуете больше классов, вероятность конфликтов имен возрастает экспоненциально, и каждый из этих конфликтов имен должен быть разрешен явным образом.
Во-вторых, более серьезная проблема – проблема ромба (или англоязычный термин – diamond problem). Она появляется, когда класс множественно наследуется от двух классов, каждый из которых наследуется от одного базового класса. Это приводит к ромбовидной структуре наследования.
Например, рассмотрим следующий набор классов:
Рисунок 2 – Ромбовидная структура наследования
Сканеры ( Scanner ) и принтеры ( Printer ) являются устройствами с питанием, поэтому они являются производными от PoweredDevice . Однако копировальный аппарат ( Copier ) включает в себя функции как сканеров, так и принтеров.
В этом контексте возникает много вопросов, в том числе, должен ли Copier иметь одну или две копии PoweredDevice , и как разрешать определенные типы неоднозначных ссылок. Хотя большинство этих проблем можно решить с помощью явного определения области видимости, затраты на поддержку, добавленные к вашим классам, чтобы справиться с дополнительной сложностью, могут привести к резкому увеличению времени разработки. Подробнее о способах решения проблемы ромба мы поговорим в следующей главе (урок «18.8 – Виртуальные базовые классы»).
Множественное наследование – больше проблем, чем оно того стоит?
Как оказалось, большинство задач, которые можно решить с помощью множественного наследования, можно решить и с помощью одиночного наследования. Многие объектно-ориентированные языки (например, Smalltalk, PHP) даже не поддерживают множественное наследование. Многие относительно современные языки, такие как Java и C#, ограничивают классы одним наследованием от обычных классов, но допускают множественное наследование от классов интерфейсов (о чем мы поговорим позже). Основная идея запрета множественного наследования в этих языках заключается в том, что это просто делает язык слишком сложным и в конечном итоге вызывает больше проблем, чем устраняет.
Многие авторы и опытные программисты считают, что множественного наследования в C++ следует избегать любой ценой из-за множества потенциальных проблем, которые оно приносит. Автор данного руководства не согласен с этим подходом, потому что бывают случаи и ситуации, когда множественное наследование – лучший способ решения задачи. Однако множественное наследование следует использовать крайне осторожно.
Интересно отметить, что вы уже использовали классы, написанные с использованием множественного наследования, даже не подозревая об этом: объекты библиотеки iostream std::cin и std::cout реализованы с использованием множественного наследования!
Правило
Избегайте множественного наследования, если альтернативы не приведут к усложнению.
Источник