Множественное наследование
Класс А обобщенно наследует элементы всех трех основных классов.
Для доступа к членам порожденного класса, унаследованного от нескольких базовых классов, используются те же правила, что и при порождении из одного базового класса. Проблемы могут возникнуть в следующих случаях:
В этих случаях необходимо использовать оператор разрешения контекста для уточнения элемента, к которому осуществляется доступ, именем базового класса.
Так как объект порожденного класса состоит из нескольких частей, унаследованных от базовых классов, то конструктор порожденного класса должен обеспечивать инициализацию всех наследуемых частей. В списке инициализации в заголовке конструктора порожденного класса должны быть перечислены конструкторы всех базовых классов. Порядок выполнения конструкторов при порождении из нескольких классов следующий:
Деструкторы вызываются в порядке обратном вызову конструкторов.
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 в объект класса А не является допустимым, существует два выхода для разрешения такой ситуации:
Базовый класс определяется как виртуальный заданием ключевого слова 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;
>
Конструкторы и деструкторы при использовании виртуальных базовых классов выполняются в следующем порядке:
При порождении с использованием виртуальных базовых классов сохраняются те же правила видимости, что и при порождении с не виртуальными классами.
1)Какие утверждения верны относительно определения множественного наследования?
Выберите один или несколько ответов:
a. При множественном наследовании могут возникнуть неоднозначности.
b. Каждый базовый класс имеет свой спецификатор доступа.
c. В иерархии множественного наследования могут быть одинаковые базовые классы.
d. В списке базовых классов допустимо повторение.
e. Для базовых классов определяется один спецификатор доступа.
2)Какие утверждения верны относительно наследования?
Выберите один или несколько ответов:
a. Иерархически последовательно можно наследовать не более двух раз
b. В иерархии наследования класс может участвовать произвольное число раз.
c. Наследование – один из трех фундаментальных принципов объектно-ориентированного программирования.
d. От одного базового класса можно наследовать один производный.
e. Класс, который наследуется, называется базовым.
3)Какие утверждения верны, когда базовый класс наследуется по спецификатору protected?
Выберите один или несколько ответов:
a. Закрытые элементы базового класса становятся защищенными в производном.
b. Защищенные элементы базового класса становятся защищенными в производном классе.
c. Закрытые элементы базового класса становятся защищенными в производном.
d. Защищенные элементы базового класса становятся открытыми в производном классе.
e. Открытые элементы базового класса становятся защищенными в производном классе.
4)Какие утверждения верны, когда базовый класс наследуется по спецификатору private?
Выберите один или несколько ответов:
a. Открытые элементы базового класса становятся закрытыми в производном классе.
b. Защищенные элементы базового класса становятся закрытыми в производном классе.
c. Закрытые элементы базового класса становятся закрытыми в производном.
d. Закрытые элементы базового класса становятся защищенными в производном.
e. Защищенные элементы базового класса становятся открытыми в производном классе.
5)Какие утверждения верны относительно указателя на объект производного класса?
Выберите один или несколько ответов:
a. Если указатель базового класса указывает на объект производного класса, а затем инкрементируется, то он будет указывать на следующий объект производного класса.
b. Значение указателя, объявленного в качестве указателя на базовый класс, можно многократно переопределять.
c. Указатель, объявленный в качестве указателя на производный класс, может использоваться, как указатель на базовый класс.
d. Указатель, объявленный в качестве указателя на базовый класс, может использоваться, как указатель на любой класс, производный от этого базового.
Помощь в написании контрольных, курсовых и дипломных работ здесь.
Урок №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, реализованы с использованием множественного наследования!
Правило: Используйте множественное наследование только в крайних случаях, когда задачу нельзя решить одиночным наследованием, либо другим альтернативным способом (без изобретения «велосипеда»).
Наследование в C++: beginner, intermediate, advanced
В этой статье наследование описано на трех уровнях: beginner, intermediate и advanced. Expert нет. И ни слова про SOLID. Честно.
Beginner
Что такое наследование?
Наследование является одним из основополагающих принципов ООП. В соответствии с ним, класс может использовать переменные и методы другого класса как свои собственные.
Класс, который наследует данные, называется подклассом (subclass), производным классом (derived class) или дочерним классом (child). Класс, от которого наследуются данные или методы, называется суперклассом (super class), базовым классом (base class) или родительским классом (parent). Термины “родительский” и “дочерний” чрезвычайно полезны для понимания наследования. Как ребенок получает характеристики своих родителей, производный класс получает методы и переменные базового класса.
Наследование полезно, поскольку оно позволяет структурировать и повторно использовать код, что, в свою очередь, может значительно ускорить процесс разработки. Несмотря на это, наследование следует использовать с осторожностью, поскольку большинство изменений в суперклассе затронут все подклассы, что может привести к непредвиденным последствиям.
Важное примечание: приватные переменные и методы не могут быть унаследованы.
Типы наследования
В C ++ есть несколько типов наследования:
Конструкторы и деструкторы
В C ++ конструкторы и деструкторы не наследуются. Однако они вызываются, когда дочерний класс инициализирует свой объект. Конструкторы вызываются один за другим иерархически, начиная с базового класса и заканчивая последним производным классом. Деструкторы вызываются в обратном порядке.
Важное примечание: в этой статье не освещены виртуальные десктрукторы. Дополнительный материал на эту тему можно найти к примеру в этой статье на хабре.
Множественное наследование
Множественное наследование происходит, когда подкласс имеет два или более суперкласса. В этом примере, класс Laptop наследует и Monitor и Computer одновременно.
Проблематика множественного наследования
Множественное наследование требует тщательного проектирования, так как может привести к непредвиденным последствиям. Большинство таких последствий вызваны неоднозначностью в наследовании. В данном примере Laptop наследует метод turn_on() от обоих родителей и неясно какой метод должен быть вызван.
Несмотря на то, что приватные данные не наследуются, разрешить неоднозначное наследование изменением уровня доступа к данным на приватный невозможно. При компиляции, сначала происходит поиск метода или переменной, а уже после — проверка уровня доступа к ним.
Intermediate
Проблема ромба
Ромбовидная проблема — прежде всего проблема дизайна, и она должна быть предусмотрена на этапе проектирования. На этапе разработки ее можно разрешить следующим образом:
Проблема ромба: Конструкторы и деструкторы
Поскольку в С++ при инициализации объекта дочернего класса вызываются конструкторы всех родительских классов, возникает и другая проблема: конструктор базового класса Device будет вызван дважды.
Виртуальное наследование
Виртуальное наследование (virtual inheritance) предотвращает появление множественных объектов базового класса в иерархии наследования. Таким образом, конструктор базового класса Device будет вызван только единожды, а обращение к методу turn_on() без его переопределения в дочернем классе не будет вызывать ошибку при компиляции.
Примечание: виртуальное наследование в классах Computer и Monitor не разрешит ромбовидное наследование если дочерний класс Laptop будет наследовать класс Device не виртуально ( class Laptop: public Computer, public Monitor, public Device <>; ).
Абстрактный класс
В С++, класс в котором существует хотя бы один чистый виртуальный метод (pure virtual) принято считать абстрактным. Если виртуальный метод не переопределен в дочернем классе, код не скомпилируется. Также, в С++ создать объект абстрактного класса невозможно — попытка тоже вызовет ошибку при компиляции.
Интерфейс
С++, в отличии от некоторых ООП языков, не предоставляет отдельного ключевого слова для обозначения интерфейса (interface). Тем не менее, реализация интерфейса возможна путем создания чистого абстрактного класса (pure abstract class) — класса в котором присутствуют только декларации методов. Такие классы также часто называют абстрактными базовыми классами (Abstract Base Class — ABC).
Advanced
Несмотря на то, что наследование — фундаментальный принцип ООП, его стоит использовать с осторожностью. Важно думать о том, что любой код который будет использоваться скорее всего будет изменен и может быть использован неочевидным для разработчика путем.
Наследование от реализованного или частично реализованного класса
Если наследование происходит не от интерфейса (чистого абстрактного класса в контексте С++), а от класса в котором присутствуют какие-либо реализации, стоит учитывать то, что класс наследник связан с родительским классом наиболее тесной из возможных связью. Большинство изменений в классе родителя могут затронуть наследника что может привести к непредвиденному поведению. Такие изменения в поведении наследника не всегда очевидны — ошибка может возникнуть в уже оттестированом и рабочем коде. Данная ситуация усугубляется наличием сложной иерархии классов. Всегда стоит помнить о том, что код может изменяться не только человеком который его написал, и пути наследования очевидные для автора могут быть не учтены его коллегами.
В противовес этому стоит заметить что наследование от частично реализованных классов имеет неоспоримое преимущество. Библиотеки и фреймворки зачастую работают следующим образом: они предоставляют пользователю абстрактный класс с несколькими виртуальными и множеством реализованных методов. Таким образом, наибольшее количество работы уже проделано — сложная логика уже написана, а пользователю остается только кастомизировать готовое решение под свои нужды.
Интерфейс
Наследование от интерфейса (чистого абстрактного класса) преподносит наследование как возможность структурирования кода и защиту пользователя. Так как интерфейс описывает какую работу будет выполнять класс-реализация, но не описывает как именно, любой пользователь интерфейса огражден от изменений в классе который реализует этот интерфейс.
Интерфейс: Пример использования
Прежде всего стоит заметить, что пример тесно связан с понятием полиморфизма, но будет рассмотрен в контексте наследования от чистого абстрактного класса.
Приложение выполняющее абстрактную бизнес логику должно настраиваться из отдельного конфигурационного файла. На раннем этапе разработки, форматирование данного конфигурационного файла до конца сформировано не было. Вынесение парсинга файла за интерфейс предоставляет несколько преимуществ.
Отсутствие однозначности касательно форматирования конфигурационного файла не тормозит процесс разработки основной программы. Два разработчика могут работать параллельно — один над бизнес логикой, а другой над парсером. Поскольку они взаимодействуют через этот интерфейс, каждый из них может работать независимо. Данный подход облегчает покрытие кода юнит тестами, так как необходимые тесты могут быть написаны с использованием мока (mock) для этого интерфейса.
Также, при изменении формата конфигурационного файла, бизнес логика приложения не затрагивается. Единственное чего требует полный переход от одного форматирования к другому — написания новой реализации уже существующего абстрактного класса (класса-парсера). В дальнейшем, возврат к изначальному формату файла требует минимальной работы — подмены одного уже существующего парсера другим.
Заключение
Наследование предоставляет множество преимуществ, но должно быть тщательно спроектировано во избежание проблем, возможность для которых оно открывает. В контексте наследования, С++ предоставляет широкий спектр инструментов который открывает массу возможностей для программиста.
Вопросы по ООП C++
1)Какие утверждения верны относительно определения множественного наследования?
Выберите один или несколько ответов:
a. При множественном наследовании могут возникнуть неоднозначности.
b. Каждый базовый класс имеет свой спецификатор доступа.
c. В иерархии множественного наследования могут быть одинаковые базовые классы.
d. В списке базовых классов допустимо повторение.
e. Для базовых классов определяется один спецификатор доступа.
2)Какие утверждения верны относительно наследования?
Выберите один или несколько ответов:
a. Иерархически последовательно можно наследовать не более двух раз
b. В иерархии наследования класс может участвовать произвольное число раз.
c. Наследование – один из трех фундаментальных принципов объектно-ориентированного программирования.
d. От одного базового класса можно наследовать один производный.
e. Класс, который наследуется, называется базовым.
3)Какие утверждения верны, когда базовый класс наследуется по спецификатору protected?
Выберите один или несколько ответов:
a. Закрытые элементы базового класса становятся защищенными в производном.
b. Защищенные элементы базового класса становятся защищенными в производном классе.
c. Закрытые элементы базового класса становятся защищенными в производном.
d. Защищенные элементы базового класса становятся открытыми в производном классе.
e. Открытые элементы базового класса становятся защищенными в производном классе.
4)Какие утверждения верны, когда базовый класс наследуется по спецификатору private?
Выберите один или несколько ответов:
a. Открытые элементы базового класса становятся закрытыми в производном классе.
b. Защищенные элементы базового класса становятся закрытыми в производном классе.
c. Закрытые элементы базового класса становятся закрытыми в производном.
d. Закрытые элементы базового класса становятся защищенными в производном.
e. Защищенные элементы базового класса становятся открытыми в производном классе.
5)Какие утверждения верны относительно указателя на объект производного класса?
Выберите один или несколько ответов:
a. Если указатель базового класса указывает на объект производного класса, а затем инкрементируется, то он будет указывать на следующий объект производного класса.
b. Значение указателя, объявленного в качестве указателя на базовый класс, можно многократно переопределять.
c. Указатель, объявленный в качестве указателя на производный класс, может использоваться, как указатель на базовый класс.
d. Указатель, объявленный в качестве указателя на базовый класс, может использоваться, как указатель на любой класс, производный от этого базового.
Помощь в написании контрольных, курсовых и дипломных работ здесь.





