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

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

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

В C++ производный класс может быть порождён из любого числа непосредственных базовых классов. Наличие у производного класса более чем одного непосредственного базового класса называется множественным наследием. Синтаксически множественное наследование отличается от единичного наследования списком баз, состоящим более чем из одного элемента.

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

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

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

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

В этом примере класс A дважды используется при объявлении класса D в качестве непрямого базового класса.

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

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

Такой фрагмент объекта мы будем называть производным фрагментом-представителем данного класса.

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

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

Вот как выглядит граф ранее приведённого в качестве примера производного класса D:

А вот как представляется структура производного класса в виде неполной схемы класса. Базовые классы располагаются на этой схеме в порядке, который соответствует списку базовых элементов в описании базы производного класса. Этот же порядок будет использован при изображении диаграмм объектов. И это несмотря на то обстоятельство, что порядок вызова конструкторов базовых классов определяется конкретной реализацией. За порядком вызова конструкторов базовых классов всегда можно наблюдать после определения их собственных версий.

А вот и схема объекта производного класса.

Первое, что бросается в глаза — это множество одноимённых переменных, «разбросанных» по базовым фрагментам объекта. Да и самих базовых фрагментов здесь немало.

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

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

Например, неоднозначность содержится в следующем операторе:

здесь предпринимается неудачная попытка изменения значения данного-члена базового фрагмента объекта MyD. Выражение доступа MyD.xA именует сразу две переменных xA. Разрешение неоднозначности сводится к построению такого выражения доступа, которое однозначно указывало бы функцию, объект, тип (об этом позже!) или перечислитель.

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

Этот оператор обеспечивает изменение значения данного-члена базового фрагмента — представителя класса B. Здесь нет никаких проблем, поскольку непосредственный базовый класс B наследует данные-члены базового класса A. Поскольку в классе B отсутствуют данные-члены с именем x0, транслятор однозначно определяет принадлежность этого элемента. Итак, доступ к данному-члену базового класса A «со стороны» непосредственного базового класса B не представляет особых проблем.

А теперь изменяется значение данного-члена базового фрагмента — представителя класса С. И опять же транслятор однозначно определяет местоположение изменяемой переменной. Переменная x0 была объявлена в непосредственном базовом классе C. И операция доступа указывает на эту переменную. А вот попытка изменения значения переменной x0, расположенной базовом фрагменте-представителе класса A «со стороны» непосредственного базового класса C обречена. Так, оператор

некорректен по причине неоднозначности соотнесения класса и его члена, поскольку непонятно, о каком базовом фрагменте-представителе класса A идёт речь. Выражения доступа с составными квалифицированными именами, как например,

в контексте нашей программы также некорректны: составное квалифицированное имя предполагает вложенное объявление класса. Это свойство операции доступа уже обсуждалось ранее, в разделах, непосредственно посвящённых операциям. Вложенные объявления будут рассмотрены ниже.

Операция :: оставляет в «мёртвой зоне» целые фрагменты объектов. Однако возможность доступа к членам класса, которые оказались вне пределов досягаемости операции доступа всё же существует. Она обеспечивается указателями и операциями явного преобразования типа.

Идея состоит в том, чтобы, объявив указатель на объект-представитель базового класса, попытаться его настроить с помощью операций явного преобразования типа на соответствующий фрагмент объекта производного класса. В результате недосягаемые с помощью операции доступа фрагменты объекта превращаются в безымянные объекты простой конфигурации. Доступ к их членам в этом случае обеспечивается обычными операциями косвенного обращения. Рассмотрим несколько строк, которые демонстрируют такую технику работы с недосягаемыми фрагментами.

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

Аналогичным образом обстоят дела с функциями-членами базовых классов. Этот раздел мы завершаем небольшой программой, демонстрирующей методы доступа к членам базовых фрагментов объекта производного класса.

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

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

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

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

  • IDrawable (то, что можно нарисовать)
  • ISerializable (то, что можно записать в файл)

Тогда класс JPGPicture будет наследоваться от этих двух интерфейсов, т. е. методы обоих интерфейсов будут реализованы в классе JPGPicture.
Интерфейсы — наиболее популярное применение множественного наследования. Возможность использования множественного наследования в виде интерфейсов есть во всех классических ООП-языках. В некоторых языках программирования (Java, C#) есть только такой вид множественного наследования.

Наследование от нескольких полноценных классов
Допустим у нас есть классы Cow и Sniper, а мы хотим получить класс, который обладает данными и методами обоих классов. Назовем его CowSniper. Таким образом у нас есть возможность «скрещивать» классы.
Таким образом синтаксически множественное наследование почти не отличается от обычного.
Заметим , что важен порядок перечисления предков, потому как в зависимости от него:

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

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

Перекрытие имен функций
эта проблема есть как и в обычном наследовании, так и в множественном.
Пусть в классах 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, которые ко всему прочему теперь являются абстрактными и невозможно создать их экземпляры. Цель достигнута.

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

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

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

При этом если создать объект класса 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; .

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

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

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

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

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

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

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

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

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

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

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

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

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

Множественное наследование из двух производных классов

У меня есть абстрактный базовый класс, который действует как интерфейс.

У меня есть два «набора» производных классов, которые реализуют половину абстрактного класса. (один «набор» определяет абстрактные виртуальные методы, связанные с инициализацией, другой «набор» определяет те, которые связаны с фактической «работой».)

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

Итак: (плохой псевдокод)

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

Вот «настоящая проблема», хотя (я немного соврал, чтобы упростить пример).

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

Потому что общая идиома состоит в том, чтобы сделать все виртуальные методы частными.

К сожалению, теперь оба AbsInit и AbsWork наследуют эти методы, и поэтому NotAbsTotal наследует «два из каждого» (я понимаю, что я могу разбить то, что действительно происходит во время компиляции).

В любом случае g++ жалуется, что: «запрос для члена init() неоднозначен» при попытке использовать класс.

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

Итак: — У меня с моей реализацией? — Является ли это ограничением идиомы для создания виртуальных методов? — Как мне реорганизовать мой код, чтобы делать то, что я хочу? (Предоставьте один общий интерфейс, но дайте возможность поменять местами реализации для «наборов» функций-членов)

Кажется, что Virtual Inheritance является решением. Раньше я слышал о виртуальном наследовании, но я не обворачивал его. Я по-прежнему открыт для предложений.

Множественное наследование, С++ и та же подпись метода в нескольких суперклассах

У меня нет опыта работы на С++, и я исхожу из фона Java. В последнее время меня спросили в интервью, почему Java не допускает множественного наследования, и ответ был довольно прост. Тем не менее, мне все еще интересно, как С++ имеет дело с этим, поскольку он позволяет наследовать более чем один класс.

В частности, скажем, что существует класс под названием MechanicalEngineer , а другой — ElectricalEngineer . Оба имеют метод под названием buildRobot() .

Что произойдет, если мы создадим третий класс RoboticsEngineer , который будет удаляться из обоих и не переопределяет этот метод, и вы просто вызываете:

Будет ли выбрано исключение, или будет использоваться метод из одного из суперклассов? Если да, то каким образом компилятор знает, какой класс использовать?

Компилятор будет отмечать такую ​​ситуацию (т.е. пытаться вызвать (some instance of RoboticsEngineer).buildRobot() ) как ошибку.

Это происходит из-за того, что производный объект получил копию обоих базовых объектов (экземпляр MechanicalEngineer и экземпляр ElectricalEngineer ) внутри себя и одна только сигнатура метода недостаточно, чтобы указать, какой из них использовать.

Если вы переопределите buildRobot в RoboticsEngineer , вы сможете явно указать, какой унаследованный метод использовать, префикс имени класса, например:

В той же самой монете вы можете «заставить» компилятор использовать одну или ту же версию из buildRobot , предварительно указав ее именем класса:

в этом случае будет реализована реализация метода ElectricalEngineer метода, без двусмысленности.

Частный случай задается, если у вас есть базовый класс Engineer как для MechanicalEngineer , так и ElectricalEngineer , и вы укажете наследование как virtual в обоих случаях. Когда используется virtual , производный объект не содержит двух экземпляров Engineer , но компилятор гарантирует, что есть только один из них. Это будет выглядеть так:

будет разрешаться без двусмысленностей. То же самое верно, если buildRobot объявлен virtual и переопределен в одном из двух производных классов. В любом случае, если оба производных класса (ElectricalEngineer и MechanicalEngineer) переопределяют buildRobot , тогда неоднозначность возникает снова, и компилятор будет отмечать попытку вызова (some instance of RoboticsEngineer).buildRobot(); в качестве ошибки.

Еще по теме:

  • Федеральным законом 418 фз Прокуратура Московской области С 1 января 2018 года на территории Российской Федерации вступил в силу Федеральный закон от 28.12.2017 № 418-ФЗ «О ежемесячных выплатах семьям, имеющим детей», которым установлены основания и порядок назначения и осуществления ежемесячной выплаты в связи с […]
  • Правила на соревнованиях по боевому самбо Правила самбо Основные правила соревнований по самбо В самбо разрешается применять броски, удержания и болевые приёмы на руки и ноги. В самбо броски можно проводить с помощью рук, ног и туловища. В самбо баллы присуждаются за броски и удержания. Бросок — это приём, с помощью […]
  • Осаго в туле 2018 Калькулятор ОСАГО в Тула на 2018 года Сделайте расчет на калькуляторе, и мы покажем вам где дешевле застраховать машину по ОСАГО в Тула Стоимость ОСАГО в компаниях: Результаты полученные на калькуляторе будут сохранены в Вашем личном кабинете. Вы всегда сможете их посмотреть и сделать […]
  • Налог на имущество за 2 квартал сроки Отчет по налогу на имущество в 2017 году Актуально на: 29 июня 2017 г. Организации, являющиеся плательщиками налога на имущество, должны отчитываться перед ИФНС по итогам отчетных периодов, а также по итогам года (ст. 386 НК РФ). Налог на имущество: отчетность в 2017 году По итогам […]
  • Что дает договор купли продажи автомобиля Что дает договор купли продажи автомобиля После оформления покупки. Одним из самых распространенных способов для продажи своего транспортного средства остается договор купли-продажи. Такой вариант особенно удобен тем, что данный документ может составить каждый человек, согласовав его как […]
  • Право на наследство опекуна у опекаемого Имеет ли право опекун на наследство опекаемого Вопросы опеки и попечительства детально урегулированы законодательством РФ. Это обеспечивает защиту прав самых уязвимых категорий граждан — недееспособных (ограниченно дееспособных) и несовершеннолетних. Но если с правами тех, кто под […]
  • Памятка по правилам поведения на водоемах Памятка по правилам поведения на водоемах В этом году весна пришла раньше обычного. Лед на реках почти весь сошел, а на закрытых водоемах ледяной покров еще остался и представляет определенную опасность. Очень опасно по нему ходить: в любой момент может рассыпаться с шипением под ногами […]
  • Закон 52-кз ставропольского края Ставропольский край: Закон № 52-кз от 27.11.2002 Ставропольский край Редакция устарела, новую редакцию можно найти здесь Внимание! В закон внесены изменения Принят Государственной Думой СК 21 ноября 2002 года (в ред. Законов СК от 16.10.2003 N 31-кз, от 07.06.2004 N 40-кз, от 29.11.2004 […]