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

Обновлено: 02.07.2024

Ява не'т разрешить множественное наследование, но это позволяет реализовывать несколько интерфейсов. Почему?

Потому что интерфейсы укажите делает только what класс, не how он делает это.

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

Один из моих преподавателей объяснил мне это так:

Предположим, у меня есть один класс, который является плита, и другой класс, который NuclearBomb. Они оба могут и "тьмы" и установка. Они оба имеют на() метод. (Есть выкл(), другие не'т.) Если я хочу создать класс, который'ы подкласс оба эти. как вы видите, это проблема, которая действительно может взорвать в моем лице здесь.

С интерфейсом, хотя вы'вновь что-то описанием класса способны делать, а не заимствование другого класса's не способ сделать что-то. Несколько интерфейсов гораздо менее вероятно, чтобы вызвать сложные конфликты, которые должны быть решены, чем несколько родительских классов.

Поскольку наследование злоупотреблять, даже если вы можете'т сказал "Эй, что способ выглядит полезным, Я'МР расширить этот класс также и".

Ответ на этот вопрос кроется во внутренней работы компилятора Java(конструктора цепочек). Если мы видим внутренние работы компилятора Java:

После компиляции это будет выглядеть примерно так:

когда мы расширяет класс и создать объект этого одной цепи конструктор будет работать до "объекта" класс.

Выше код будет работать нормально. но если у нас есть еще один класс под названием "Автомобиль", которое расширяет "банк" и один гибрид(множественное наследование) классов под названием SBICar :

В этом случае(SBICar) не удастся создать цепочку конструктор(компиляции двусмысленность).

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

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

Надеюсь, что это решит ваш запрос. Спасибо.

Реализация нескольких интерфейсов-это очень полезно и не'т вызвать много проблем для разработчиков языков, ни программистов. Так это разрешено. Множественное наследование, а также полезным, может вызвать серьезные проблемы для пользователей (страшный Алмаз смерти). И большинство вещей, которые вы делаете с множественным наследованием можно также сделать композицию или используя внутренние классы. Так что множественное наследование запрещено, как приносить больше проблем, чем выгод.

Java поддерживает множественное наследование через интерфейсы. Класс может реализовывать любое количество интерфейсов, но можно расширить только один класс.

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

В "Белой книге" под названием “в Java: обзор” Джеймсом Гослингом в феврале 1995 года([ссылка][1]) дает представление о том, почему множественное наследование не поддерживается в Java.

По словам Гослинга:

и"Ява опускает многие редко используемые, плохо понимал, запутанным особенности C++, который, по нашему опыту, приносят больше горя, чем пользы. Это В основном состоит из перегрузка операторов (хотя у него есть перегрузка метода), множественное наследование, и обширная автоматическая приведения.&и"

Вы можете найти точный ответ на этот запрос в разделе документации по Oracle о множественное наследование

  1. Множественное наследование государства: возможность наследовать поля из нескольких классов

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

Если разрешено множественное наследование, и при создании объекта путем создания экземпляра этого класса, этот объект наследует все поля класса'ы суперклассов. Это вызовет два вопроса.

Что делать, если методы или конструкторы из разных супер-классы создания той же области?

Какой метод или конструктор будет иметь приоритет?

Множественное наследование реализации: возможность унаследовать определения метода из нескольких классов

Проблемы с этим подходом: название конфликтаTS и неопределенность. Если подкласс и суперкласс содержат одинаковое имя метода (и подпись), компилятор может'т определить, какую версию использовать.

Но Java поддерживает этот тип множественного наследования с методы по умолчанию, которые были введены с версии Java 8. Компилятор Java предусмотрены некоторые правила, чтобы определить, какой метод по умолчанию для определенного класса применений.

Смотрите ниже SE пост для получения более подробной информации по решению проблемы алмазов:

  1. Множественное наследование типа: способность класса, чтобы реализовать более одного интерфейса.

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

Урок, извлеченный из C++ ж/ множественное inheritence, что это приводит к больше проблем, чем она стоит.

Интерфейс-это контракт, чего ваш класс должен реализовать. Вы Don'т получить любую функциональность интерфейс. Наследованию позволяет наследовать функциональность родительского класса (и в нескольких наследованию, что может стать очень запутанным).

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

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

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

Рассмотрим следующий класс:

В этом случае класс ABC не выходит ничего? Не так быстро, этот класс неявные расширяет объект класса, базовый класс, который позволит всем работать в Java. Все является объектом.

Если вы пытаетесь использовать класс выше вы'увидите, что ваш IDE позволяют использовать такие методы, как: равен(объект o) , метод toString () и т. д., Но вы не'т объявить эти методы, они пришли из базового класса объект

Вы могли бы попробовать:

Это нормально, потому что ваш класс не будет неявный объект расширяется, но расширяется строка потому что вы это сказали. Рассмотрим следующее изменение:

Теперь ваш класс всегда будет возвращать "Привет" Если вы вызываете метод toString().

А теперь представьте следующий класс:

Снова класс флаер неявное расширяет объект, который имеет метод метод toString() , любой класс будет иметь этот метод, так как они все увеличивают "объект" косвенно, так что, если вы называете метод toString() от птица , которые метод toString() Ява бы использовать? От букваря или флаер ? Это произойдет с любым классом, которые пытаются распространяется на два или несколько классов, чтобы избежать такого рода "и способ столкновения" они составили идеи интерфейс, в принципе, вы могли бы думать их как абстрактный класс, который не распространяется объект косвенно. Так как они аннотация они должны быть реализованы с помощью класса, который является объект (вы не можете instanciate интерфейс в одиночку, они должны быть реализованы в классе), так что все будет продолжать нормально работать.

Отличаются классы от интерфейсов Сайта осуществляет был зарезервирован только для интерфейсов.

Множественное наследование — это одна из ключевых особенностей языка C++. Рассмотрим, когда оно может потребоваться и как его использовать.

Назначение множественного наследования

Предположим, что нам нужно нарисовать на форме логотип, который состоит из квадрата и круга. Сначала посмотрим, как это сделать на Си. Предположим, что у нас уже есть функции рисования квадрата и круга.

Далее везде, где нужно рисование логотипа, вызываем функцию Logo.

Теперь нам нужно то же самое сделать в C++. Предположим, что у нас уже есть классы рисования квадрата и круга. Как нарисовать логотип?

Можно, конечно, сделать так:

А потом вызывать везде эту пару строк, но это процедурный подход и он в C++ не приветствуется. В тех случаях, когда нам нужно получить методы разных классов, как раз и используется множественное наследование.

Использование множественного наследования

Для поддержки множественного наследования нужно при определении класса перечислить через запятую базовые классы. В примере для простоты вместо рисования фигур методы Draw просто выводят их названия.

Отличия множественного наследования

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


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

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


Ошибка неоднозначности

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

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

Алмаз смерти

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


В рассмотренном примере добавим суперкласс Shape.

Мы сразу получаем множество проблем неоднозначности вызова. Так, например, если мы видим в классе Logo координаты (x, y) , то какие именно координаты мы имеем в виду?

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

В конце концов если классы пишет один человек, то он сам все-таки представляет, что делать в случае неоднозначности вызова. Но если это используется в группе, то ситуация резко усложняется.

Лучше всего эту картину иллюстрирует карточный домик.


Представьте себе, что каждая карта — это класс, который разрабатывает отдельный программист. Нижние карты — это базовые классы, а все остальные — производные классы. И теперь представьте, что вам достался самый верхний этаж.

Запрет наследования

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

В этот момент к вам подходит коллега и говорит:

— Слушай, я унаследовал твой класс и класс Угрюмова. И получил алмаз смерти. Иди и решай с Угрюмовым проблему неоднозначности.

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

— А может ты не будешь наследовать мой класс?!

Для подобных случаев в стандарт C++ 11 добавлен спецификатор final, который запрещает наследовать данный класс. Для этого нужно написать ключевое слово final после имени класса.


В отличие от своего объектно-ориентированного предшественника — языка С++, в языке Java не поддерживается множественное наследование (МН). Но рано или поздно любой программист, использующий парадигму объектно-ориентированного программирования (ООП), сталкивается с необходимостью опираться на методы от разных родительских классов. А опыт применения МН в С++ показал, что это приводит к большему количеству проблем, чем ожидалось. Поэтому в рамках построения парадигмы ООП создатели JAVA подошли с других позиций. Так как же этот вопрос был решён в JAVA?

Для начала рассмотрим как исторически развивался вопрос о проблемах наследования на примере С++.

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

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

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

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

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

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

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

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

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

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

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

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

Однако эту проблему можно частично решить с помощью интерфейсов.

Другими словами, для каждого класса в Java может существовать только один родительский класс. Тем не менее в каждом классе можно реализовать произвольное количество интерфейсов.

При этом данный класс будет соответствовать типам всех тех интерфейсов, которые в нем реализованы.

Как видите, с помощью интерфейсов создаются новые типы объектов без их реализации.

Как известно, в абстрактном классе допускается реализация некоторых методов, не объявленных абстрактными. В отличие от них, интерфейсы — это чистой воды шаблоны. С помощью интерфейса можно только определить функциональность, но не реализовать ее (исключения составляют дефолтные методы, которые появились в Java 8).

Чтобы определить интерфейс, используется ключевое слово interface. Например:

Нужно обратить внимание, что интерфейсы — это не классы, хотя и очень похожи на них.

При реализации интерфейса в классе имя интерфейса указывается в объявлении этого класса после ключевого слова implements. После этого процесс реализации интерфейса станет точно таким же, как расширение абстрактного класса, который содержит только абстрактные методы.

Данный интерфейс называется Printable. Интерфейс может определять константы и методы, которые могут иметь, а могут и не иметь реализации. Методы без реализации похожи на абстрактные методы абстрактных классов. Так, в данном случае объявлен один метод, который не имеет реализации.

Все методы интерфейса не имеют модификаторов доступа, но фактически по умолчанию доступ public, так как цель интерфейса — определение функционала для реализации его классом. Поэтому весь функционал должен быть открыт для реализации.

В данном случае класс Book реализует интерфейс Printable. При этом надо учитывать, что если класс применяет интерфейс, то он должен реализовать все методы интерфейса, как в случае выше реализован метод print. Потом в методе main мы можем объект класса Book и вызвать его метод print. Если класс не реализует какие-то методы интерфейса, то такой класс должен быть определен как абстрактный, а его неабстрактные классы-наследники затем должны будут эти методы реализовать.

В тоже время мы не можем создавать объекты интерфейсов, поэтому следующий код не будет работать:

Одним из преимуществ использования интерфейсов является то, что они позволяют добавить в приложение гибкости. Например, в дополнение к классу Book определим еще один класс, который будет реализовывать интерфейс Printable:

Класс Book и класс Journal связаны тем, что они реализуют интерфейс Printable. Поэтому мы динамически в программе можем создавать объекты Printable как экземпляры обоих классов:

Интерфейсы в преобразованиях типов

Интерфейс — это ссылочный тип. Например, так как класс Journal реализует интерфейс Printable, то переменная типа Printable может хранить ссылку на объект типа Journal:

И если мы хотим обратиться к методам класса Journal, которые определены не в интерфейсе Printable, а в самом классе Journal, то нам надо явным образом выполнить преобразование типов:

Методы по умолчанию

Ранее до JDK 8 при реализации интерфейса мы должны были обязательно реализовать все его методы в классе. А сам интерфейс мог содержать только определения методов без конкретной реализации. В JDK 8 была добавлена такая функциональность как методы по умолчанию. И теперь интерфейсы кроме определения методов могут иметь их реализацию по умолчанию, которая используется, если класс, реализующий данный интерфейс, не реализует метод. Например, создадим метод по умолчанию в интерфейсе Printable:

Метод по умолчанию — это обычный метод без модификаторов, который помечается ключевым словом default. Затем в классе Journal нам необязательно этот метод реализовывать, хотя мы можем его и переопределить:

Статические методы

Начиная с JDK 8 в интерфейсах доступны статические методы — они аналогичны методам класса:

Чтобы обратиться к статическому методу интерфейса также, как и в случае с классами, пишут название интерфейса и метод:

Приватные методы

По умолчанию все методы в интерфейсе фактически имеют модификатор public. Однако начиная с Java 9 мы также можем определять в интерфейсе методы с модификатором private. Они могут быть статическими и нестатическими, но они не могут иметь реализации по умолчанию.

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

Константы в интерфейсах

Кроме методов в интерфейсах могут быть определены статические константы:

Хотя такие константы также не имеют модификаторов, но по умолчанию они имеют модификатор доступа public static final , и поэтому их значение доступно из любого места программы.

Множественная реализация интерфейсов

Если нам надо применить в классе несколько интерфейсов, то они все перечисляются через запятую после слова implements:

Наследование интерфейсов

Интерфейсы, как и классы, могут наследоваться:

При применении этого интерфейса класс Book должен будет реализовать как методы интерфейса BookPrintable, так и методы базового интерфейса Printable.

Вложенные интерфейсы

Как и классы, интерфейсы могут быть вложенными, то есть могут быть определены в классах или других интерфейсах. Например:

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

Использование интерфейса будет аналогично предыдущим случаям:

Интерфейсы как параметры и результаты методов

И также как и в случае с классами, интерфейсы могут использоваться в качестве типа параметров метода или в качестве возвращаемого типа:

Метод read() в качестве параметра принимает любой объект, реализующий интерфейс Printable, поэтому в этот метод мы можем передать как объект Book, так и объект Journal.

Метод createPrintable() возвращает объект, реализующий Printable, поэтому также мы можем возвратить как объект Book, так и Journal.

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

https://javadevblog.com/wp-content/uploads/2015/05/multiinh.jpg

Давайте создадим абстрактный суперкласс SuperClass с методом doSomething(), а также два класса ClassA, ClassB

А теперь давайте создадим класс ClassC, который наследует классы ClassA и ClassB

Обратите внимание, что метод test() вызывает метод суперкласса doSomething()

Это приводит к неопределенности: компилятор не знает, какой метод суперкласса выполнить из-за ромбовидной формы (выше на диаграмме классов). Это и называют проблемой ромба — и это основная причина почему Java не поддерживает множественное наследование классов.

Множественное наследование в интерфейсах

МН не поддерживается в классах, но оно поддерживается в интерфейсах и единый интерфейс может наследовать несколько интерфейсов, ниже простой пример.

Обратите внимание, что в обоих интерфейсах объявлен такой же метод, а теперь посмотрим, что с этого получится:

И это отличный выход, потому что интерфейсы только объявляют методы, а фактическая реализация будет сделана в конкретных классах, которые реализуют интерфейсы, так что нет никакой возможности двусмысленно трактовать множественное наследование в интерфейсе. Все усложняется, если некий класс реализует более одного (скажем, два) интерфейса, а они реализуют один и тот же самый метод по умолчанию. Какой из методов унаследует класс? Ответ — никакой. В таком случае класс должен реализовать метод самостоятельно (напрямую, либо унаследовав его от другого класса).

Теперь давайте посмотрим на код ниже:

Используем Композицию (Composition)?

Так что же делать, если мы хотим использовать метод methodA() класса ClassA и метод methodB() класса ClassB в ClassC?

Решение заключается в использовании композиции. Ниже представлена версия класса ClassC с использованием композиции:

Так что же использовать: Композицию или Наследование?

Предположим, у нас есть суперкласс и подкласс:

Код выше компилируется и работает нормально, но что будет, если реализация класса ClassC изменяется, как показано ниже:

Обратите внимание, что метод test() уже существует в подклассе, но тип возвращаемого отличается, теперь ClassD не будет компилироваться, и если вы используете какую-то IDE, то вам будет предложено изменить тип возвращаемого значения на тип суперкласса или подкласса.

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

Указанная проблема никогда не произойдет с композицией, поэтому это делает её предпочтительней, чем наследование.

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

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

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

Результат выполнения этой программы:

Эта гибкость в вызове методов не доступна в наследовании.

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

Константин Кишкин, после окончания базового курса, июль 2020

Множественное наследование (Multiple inheritance)— наследование от нескольких базовых классов одновременно.

Зачем оно может использоваться

  • IDrawable (то, что можно нарисовать)
  • ISerializable (то, что можно записать в файл)
  • представление объекта класса в памяти;
  • очередность вызова конструкторов и деструкторов.

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

img

Перекрытие имен функций
эта проблема есть как и в обычном наследовании, так и в множественном.
Пусть в классах Cow и Sniper были методы sleep(). Тогда код:

В строке 2 произойдет ошибка компиляции, т. к. компилятор не может выбрать какой метод sleep() ему вызывать (от Cow или от Sniper). Поэтому необходимо сообщимть ему правильный выбор: cs.Cow::sleep();

Перекрытие виртуальных функций
Теперь рассмотрим тот случай, если метод sleep() виртуальный в классах-предках.

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

В строках 3 и 4 вызовется один и тот же метод CowSniper::sleep();
С одной стороны это удобно, т. к. если методы называются одинаково, то скорее всего они делают что-то похожее. Тогда перегрузив один метод мы перегрузим сразу оба.
С другой стороны может возникнуть проблема, если класс-потомок должен реализовывать один и тот же виртуальный метод от нескольких базовых классов по-разному. Тогда такая перегрузка будет вредна, но стандартно по другому не поступить. В таких случаях используется следующий способ обойти это ограничение.
Если есть иерархия классов A,B,C. И в классах A и B есть некоторый виртуальный метод f() .

Добавим в эту иерархию еще два класса A1 и B1. В классах A1 и B1 создадим методы fA() и fB() соотвественно. Теперь в C будут методы fA() и fB() , которые необходимо перегрузить, причем код в них разный ;)

Теперь использовать эти классы можно так:

В строке 5 вызовется C::fA() , а в строке 6 - C::fB() . Обратите внимание на то, как перегружен метод f() в классах A1 и B1, которые ко всему прочему теперь являются абстрактными и невозможно создать их экземпляры. Цель достигнута.

Представление объекта в памяти
а) Как мы помним, в случае линейного наследования распределение полей классов для объекта класса наследника в памяти будет такое:
При этом если мы создадим три указателя:

img

То указывать они все будут на начало объекта в памяти:
и при этом все хорошо.

б) Теперь перейдем к рассмотрению простейшего примера множественного наследования:

При этом если создать объект класса C, то в памяти он будет выглядеть так:

Опять попробуем создать три указателя как и в случае линейного наследования:

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

Мы видим, что классы A и B фактически разделены, но ведь если они имеют виртуальные методы, то должны вызываться перегруженные виртуальные методы класса C. Для того, чтобы это происходило в каждом из них есть ссылка на таблицу виртуальных функций. Тогда получается что в классе C будет сразу два указателя на две различные таблицы виртуальных функций (кол-во указателей = кол-ву полиморфных предков). Получается, что таблиц виртуальных функций две. Это не совсем так, т. к. они просто лежат рядом в памяти, т. е. фактически таблица одна, но в ней могут быть повторения, например, виртуальный деструктор:

в) Теперь рассмотрим множественного наследования, если в графе наследования есть цикл:

В памяти объект класса D будет представлен так:

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

Рассмотрим два случая такого наследования и посмотрим на возможные проблемы с наличием двойного объекта.
1) Такой эффект может быть полезен, если класс A является файловым потоком, а B и C это writer и reader соответственно. А класс D читает из С и пишет в B. Очевидно, что у B и C файлы могут быть различны (скорее всего так и есть).
2) Рассмотрим умный указатель (A = LinkCounter), который внутри себя сдержит счетчик. В таком случае в классе D возникает два счетчика, что может привести к печальным последствием, если в одном месте работать с одним из них, а в другом с другим.

Вообще такое наследование (с циклами в графе родства) называется бриллиантовым (diamond inheritance) или Ромбовидным.

Виртуальное наследование.
Рассмотрим его на примере следующей иерархии

Синтаксически виртуальное наследование почти не отличается от множественного:

При этом следующий код будет прекрасно работать:
А все потому, что классы будут выглядеть следующим образом:

Здась может появиться проблема преобразования указателя на D к указателю на C. Но этой проблемы нет. Необходимо разобраться каким образом это реализуется.
Дело в том, что при виртуальном наследовании добавляется виртуальная функция, возвращающая указатель на A. Фактически для программиста она не видна. Для пояснения обозначим ее за getA(). Стоит заметить что она будет различна в классах B,C и D.
Теперь при обращении к полю из A (допустим в нем поле int k ) код вида k = 10; будет автоматически преобразован в getA()->k = 10; .

img

Теперь рассмотрим очередность вызова конструкторов.
Логично предположить, что вызов конструкторов пройдет так
Но возникает проблема, ведь конструкторы B и C могут вызывать различные конструкторы A и с различными параметрами.
Для определенности было введено следующее правило: Конструктор A должен быть явно вызван в конструкторе D, при этом в конструкторах B и C вызов конструктора A опустится.

В связи с этим есть замечание: Нужно следить и понимать, что при виртуальном наследовании в конструкторах B и C может не вызваться конструктор A с разными параметрами.

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

Возможной заменой множественного наследования не от интерфейсов является агрегация:

Читайте также: