Урок №169. Виртуальный базовый класс

  Юрий  | 

  |

  Обновл. 15 Сен 2021  | 

 53547

 ǀ   6 

На уроке о множественном наследовании мы говорили о проблеме «алмаза смерти». На этом уроке мы продолжим эту тему.

Алмаз смерти

Код из того же урока, иллюстрирующий «алмаз смерти» (мы добавили еще конструкторы):

Хотя вы можете ожидать, что диаграмма наследования будет следующая:

На самом деле, это не так. Если вы создадите объект класса Copier, то получите две копии класса PoweredDevice: одну от Printer и одну от Scanner.

Диаграмму получим следующую:

Рассмотрим пример в коде:

Результат:

PoweredDevice: 3
Scanner: 1
PoweredDevice: 3
Printer: 2

Как вы видите, PoweredDevice создается дважды. Иногда так и нужно, а иногда нужно, чтобы была одна копия PoweredDevice: общая как для Scanner, так и для Printer.

Виртуальные базовые классы


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

Теперь, при создании класса Copier, мы получим только одну копию PoweredDevice, которая будет общей как для Scanner, так и для Printer.

Следует вопрос: «Если Scanner и Printer совместно используют родительский класс PoweredDevice, то кто ответственный за его создание?». Оказывается, Copier. Конструктор Copier отвечает за создание объекта PoweredDevice. Это один из тех случаев, когда дочернему классу разрешено вызывать конструктор родительского класса, который не является его непосредственным родителем:

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

PoweredDevice: 3
Scanner: 1
Printer: 2

Здесь уже PoweredDevice создается только один раз.

Обсудим несколько деталей.

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

Во-вторых, конструкторы Scanner и Printer по-прежнему вызывают конструктор PoweredDevice. При создании объекта Copier эти вызовы конструктора просто игнорируются, так как именно Copier отвечает за создание PoweredDevice, а не Scanner или Printer. Однако, если бы мы создавали объекты Scanner или Printer, то эти конструкторы вызывались бы и применялись обычные правила наследования.

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

Оценить статью:

Звёзд: 1Звёзд: 2Звёзд: 3Звёзд: 4Звёзд: 5 (241 оценок, среднее: 4,90 из 5)
Загрузка...

Комментариев: 6

  1. WiseWanderer:

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

  2. rutrud:

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

    1. Андрей:

      Нет, это нужно, чтоб избежать ошибки неоднозначности.
      Если ты в самом дочернем классе обратишься к переменной, которая есть в базовом классе, то к какой именно переменной из двух,
      B::A::m или C::A::m ?
      Т.к. m попадает и в B::A и в C::A (где A — базовый класс и для B и для C),
      то нет однозначности, какую из этих 2 переменных использовать в наследнике D (его родители B и C).
      Получишь что-то типа "Reference to m is ambiguous".

  3. Мгер:

    Вроде где то тут была ссылка на сайт майкрософт, где разбирались нюансы. Могли бы скинуть?

  4. Danila:

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

    1. Фото аватара Юрий:

      Пожалуйста 🙂

Добавить комментарий для Мгер Отменить ответ

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