1. Главная страница » Компьютеры

C множественное наследование классов

Автор: | 16.12.2019

Если порожденный класс наследует элементы одного базового класса, то такое наследование называется одиночным . Однако, возможно и множественное наследование. Множественное наследование позволяет порожденному классу наследовать элементы более, чем от одного базового класса. Синтаксис заголовков классов расширяется так, чтобы разрешить создание списка базовых классов и обозначения их уровня доступа:

Класс А обобщенно наследует элементы всех трех основных классов.

Для доступа к членам порожденного класса, унаследованного от нескольких базовых классов, используются те же правила, что и при порождении из одного базового класса. Проблемы могут возникнуть в следующих случаях:

  • если в порожденном классе используется член с таким же именем, как в одном из базовых классов;
  • когда в нескольких базовых классах определены члены с одинаковыми именами.

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

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

  • конструкторы базовых классов в порядке их задания;
  • конструкторы членов, являющихся объектами класса;
  • конструктор порожденного класса.

Деструкторы вызываются в порядке обратном вызову конструкторов.

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 .

Конструкторы и деструкторы при использовании виртуальных базовых классов выполняются в следующем порядке:

  • конструкторы виртуальных базовых классов выполняются до конструкторов не виртуальных базовых классов, независимо от того, как эти классы заданы в списке порождения;
  • если класс имеет несколько виртуальных базовых классов, то конструкторы этих классов вызываются в порядке объявления виртуальных базовых классов в списке порождения;
  • деструкторы виртуальных базовых классов выполняются после деструкторов не виртуальных базовых классов.

При порождении с использованием виртуальных базовых классов сохраняются те же правила видимости, что и при порождении с не виртуальными классами.

Мно́жественное насле́дование — свойство, поддерживаемое частью объектно-ориентированных языков программирования, когда класс может иметь более одного суперкласса (непосредственного класса-родителя), интерфейсы поддерживают множественное наследование во многих языках программирования. Эта концепция является расширением «простого (или одиночного) наследования» (англ. single inheritance ), при котором класс может наследоваться только от одного суперкласса.

В список языков, поддерживающих множественное наследование, входят: Io, Eiffel, C++, Dylan, Python, некоторые реализации классов JavaScript (например, dojo.declare), Perl 6, Curl, Common Lisp (благодаря CLOS), OCaml, Tcl (благодаря Incremental Tcl) [1] , а также Object REXX (за счёт использования классов-примесей).

Содержание

Обзор [ править | править код ]

Множественное наследование позволяет классу перенимать функциональность у множества других классов, как например, класс StudentMusician может наследовать от класса Person , класса Musician и класса Worker , что сокращённо можно написать:

Неопределённость при множественном наследовании, как в примере выше, возникает если, к примеру, класс Musician наследует от классов Person и Worker , а класс Worker , в свою очередь, наследует от Person ; подобная ситуация называется ромбовидным наследованием. Таким образом, у нас получаются следующие правила:

Если компилятор просматривает класс StudentMusician, то ему необходимо знать, нужно ли объединять возможности классов или они должны быть раздельными. Например, логично будет присоединить «Age» (возраст) класса Person к классу StudentMusician. Возраст человека не меняется, если вы рассматриваете его как Person (человек), Worker (рабочий) или Musician (музыкант). Однако, будет довольно логичным отделить свойство «Name» (имя) в классах Person и Musician, если они используют сценический псевдоним, отличающийся от настоящего имени. Варианты объединения и разделения вполне корректны для каждого из собственных контекстов и только программист знает, какой вариант является правильным для проектируемого класса.

Языки обладают различными способами разрешения таких проблем вложенного наследования, например:

  • Eiffel предоставляет программисту возможность явным образом объединить или разделить унаследованные элементы от суперклассов. Eiffel автоматически объединит элементы, если у них будет одинаковое имя и реализация. Автор класса имеет возможность переименовать наследуемые элементы для их разделения. Кроме того, Eiffel позволяет явным образом выполнять повторное наследование вида A: B, B .
  • C++ требует, чтобы программист указал, элемент какого из родительских классов должен использоваться, то есть «Worker::Person.Age». C++ не поддерживает явно повторяемое наследование, так как отсутствует способ определить какой именно суперкласс следует использовать (смотри критику). C++, также, допускает создание единственного экземпляра множественного класса благодаря механизму виртуального наследования (например, « Worker::Person » и « Musician::Person » будут ссылаться на один и тот же объект).
  • Perl использует список классов для наследования в указанном порядке. Компилятор использует первый метод, который он находит при глубинном поиске в списке суперклассов или использовании C3-линеаризации иерархии классов. Различные расширения обеспечивают альтернативные схемы композиции классов.
  • Python имеет синтаксическую поддержку для множественного наследования, а порядок базовых классов определяется алгоритмом C3-линеаризации[2] .
  • Common Lisp Object System предусматривает полный контроль методов комбинации со стороны программиста, а если этого не достаточно, то метаобъектный протокол (Metaobject Protocol) дает программисту возможность модифицировать наследование, динамическое управление, реализация класса и другие внутренние механизмы без опасения повлиять на стабильность системы.
  • Logtalk поддерживает оба интерфейса и реализацию мультинаследования, предусматривая объявление метода алиасов, поддерживающего как переименование, так и доступ к методам, которые могут оказаться недоступными, благодаря механизму разрешения конфликтов.
  • Curl допускает только такие классы, которые явным образом отмечены как доступные для повторного наследования. Доступные классы должны определять вторичный конструктор для каждого обычного конструктора класса. Сначала вызывается обычный конструктор, статус доступного класса инициализируется за счет конструктора подкласса, а вторичный конструктор вызывается для всех остальных подклассов.
  • Ocaml выбирает последнее совпавшее определение в списке наследования классов для определения метода реализации, используемого в случае неопределенности. Для переопределения поведения по умолчанию нужно просто указать метод, вызываемый при определении предпочитаемого класса.
  • Tcl допускает существование множества родительских классов — их последовательность влияет на разрешение имен членов класса. [3]
  • Delphi с версии 2007 позволяет частично реализовать множественное наследование с помощью помощников классов ( > [источник не указан 1815 дней] .

Smalltalk, C#, Objective-C, Java, Nemerle и PHP не допускают множественного наследования, что позволяет избежать многих неопределенностей. Однако, они, кроме Smalltalk, позволяют классам реализовать множественные интерфейсы. Кроме того, PHP и Ruby позволяют эмулировать множественное наследование за счет использования примесей (traits в PHP и mixins в Ruby), которые, как и интерфейсы, полноценными классами не являются. Множественное наследование интерфейсов позволяет расширить ограниченные возможности.

Критика [ править | править код ]

Множественное наследование критикуется за следующие проблемы, возникающие в некоторых языках, в частности, C++:

  • семантическая неопределённость часто совокупно представляется как Проблема ромба[4] .
  • отсутствует возможность непосредственного многократного наследования от одного класса. [источник не указан 3637 дней]
  • порядок наследования изменяет семантику класса. [источник не указан 3637 дней] Конструктор дочернего класса вызывает конструкторы непосредственных родителей, а те, в свою очередь — конструктор прародителя. Однако прародительский объект имеется в единственном экземпляре и конструировать его дважды нельзя, поэтому сработает вызов конструктора прародителя только конструктором первого родительского класса в списке наследования.

Множественное наследование в языках с конструкторами в стиле C++/Java усиливает проблему наследования конструкторов и последовательностей конструкторов, таким образом создавая проблемы с поддержкой и расширяемостью в этих языках. Объекты в отношениях наследования со значительно отличающимися методами конструирования довольно трудны для реализации в рамках парадигмы последовательности конструкторов.

Тем не менее, существуют языки, обрабатывающие эти технические тонкости (например Eiffel).

Существует мнение, что множественное наследование — это неверная концепция, порождённая неверным анализом и проектированием. В частности, для приведённого выше примера справедлив следующий вариант проектирования. Класс Person включает в себя один и более объектов класса Profession. Классы Student и Musician наследуют от Profession. Таким образом, StudentMusician будет представлен объектом класса Person содержащим объекты класса Student и Musician. Формально множественное наследование можно перепроектировать путём введения класса, являющегося «метаклассом» классов, от которых должно происходить множественное наследование. В приведённом примере таким метаклассом является Profession — профессия.

Обновл. 25 Июн 2019 |

До сих пор мы рассматривали только одиночные наследования, когда дочерний класс имеет только одного родителя. Однако C++ предоставляет возможность выполнения множественного наследования.

Множественное наследование

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

Множественное наследование может быть использовано для создания класса Teacher, который будет наследовать свойства как Human, так и Employee. Для использования множественного наследования нужно просто указать через запятую тип наследования и второй родительский класс:

Проблемы с множественным наследованием

Хотя множественное наследование кажется простым расширением одиночного наследования, оно может привести к множеству проблем, которые могут заметно увеличить сложность программ и сделать кошмаром дальнейшую поддержку кода. Рассмотрим некоторые из подобных ситуаций.

Во-первых, может возникнуть неоднозначность, когда несколько родительских классов имеют метод с одним и тем же именем. Например:

При компиляции c54G.getID() компилятор смотрит, есть ли у WirelessAdapter метод getID(). Этого метода у него нет, поэтому компилятор двигается по цепочке наследования вверх и смотрит есть ли этот метод в каком-либо из родительских классов. И здесь возникает проблема — getID() есть как у USBDevice, так и у NetworkDevice. Следовательно, вызов этого метода приведёт к неоднозначности и мы получим ошибку, так как компилятор не будет знать какую версию getID() вызывать.

Тем не менее, есть способ обойти эту проблему. Мы можем явно указать, какую версию getID() следует вызывать:

Хотя это решение довольно простое, но всё может стать намного сложнее, если наш класс будет иметь от 4 родительских классов, которые, в свою очередь, будут иметь свои родительские классы. Возможность возникновения конфликтов имён увеличивается экспоненциально с каждым добавленным родительским классом и в каждом из таких случаев нужно будет явно указывать версии методов, которые следует вызывать, дабы избежать возможности возникновения конфликтов имён.

Во-вторых, более серьёзной проблемой является «алмаз смерти» (или ещё «алмаз обреченности»). Это ситуация, когда один класс имеет 2 родительских класса, каждый из которых, в свою очередь, наследует свойства одного и того же родительского класса. Иллюстративно мы получаем форму алмаза.

Например, рассмотрим следующие классы:

Сканеры и принтеры — это устройства, которые получают питание от розетки, поэтому они наследуют свойства PoweredDevice. Однако копировальный аппарат включает в себя функции как сканеров, так и принтеров.

В этом контексте возникает много проблем, включая неоднозначность при вызове методов и копирование данных PoweredDevice в класс Copier дважды. Хотя большинство из этих проблем можно решить с помощью явного указания, поддержка и обслуживание такого кода может привести к непредсказуемым временным затратам. Мы поговорим детальнее о способах решения проблемы «алмаза смерти» в одном из следующих уроков.

Так стоит ли использовать множественное наследование?

Большинство задач, решаемых с помощью множественного наследования, могут быть решены и с использованием одиночного наследования. Многие объектно-ориентированные языки программирования (например: Smalltalk, PHP) даже не поддерживают множественное наследование. Многие, относительно современные языки, такие как Java и C#, ограничивают классы одиночным наследованием обычных классов, но допускают множественное наследование интерфейсных классов (об этом поговорим позже). Суть идеи, запрещающей множественное наследование в этих языках, заключается в том, что это лишняя сложность, которая порождает больше проблем, чем удобств.

Многие опытные программисты считают, что множественное наследование в C++ следует избегать любой ценой из-за потенциальных проблем, которые могут возникнуть. Однако всё же остаётся вероятность, когда множественное наследование будет лучшим решением, нежели придумывание двухуровневых костылей.

Стоит отметить, что вы сами уже использовали классы, написанные с использованием множественного наследования, даже не подозревая об этом: такие объекты как std::cin и std::cout библиотеки iostream реализованы с использованием множественного наследования!

Правило: Используйте множественное наследование только в крайних случаях, когда задачу нельзя решить одиночным наследованием, либо другим альтернативным способом (без изобретения «велосипедов»).

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

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