Стенли Липпман - Язык программирования C++. Пятое издание
void foo(int); // по умолчанию открыты
protected:
int ival;
char cval;
};
class VMI : public Derived1, public Derived2 { };
18.3.5. Конструкторы и виртуальное наследование
При виртуальном наследовании виртуальный базовый класс инициализируется конструктором самого последнего производного класса. В рассматриваемом примере при создании объекта класса Panda инициализацию членов базового класса ZooAnimal контролирует конструктор класса Panda.
Чтобы понять это правило, рассмотрим происходящее при применении обычных правил инициализации. В этом случае объект виртуального базового класса мог бы быть инициализирован несколько раз. Он был бы инициализирован вдоль каждой ветви наследования, содержащей этот виртуальный базовый класс. В данном примере, если бы к классу ZooAnimal применялись обычные правила инициализации, то части Bear и Raccoon инициализировали бы часть ZooAnimal объекта класса Panda.
Конечно, каждый базовый класс в иерархии объекта мог бы в некоторый момент быть "более производным". Поскольку вполне можно создавать независимые объекты класса, производного от виртуального базового класса, конструкторы в этом классе должны инициализировать его виртуальный базовый класс. Например, когда в рассматриваемой иерархии создается объект класса Bear (или Raccoon), никакого дальнейшего применения производного класса нет. В данном случае конструкторы класса Bear (или Raccoon) непосредственно инициализируют базовую часть ZooAnimal, как обычно:
Bear::Bear(std::string name, bool onExhibit) :
ZooAnimal(name, onExhibit, "Bear") { }
Raccoon::Raccoon(std::string name, bool onExhibit) :
ZooAnimal(name, onExhibit, "Raccoon") { }
Когда создается объект класса Panda, он является наиболее производным типом и контролирует инициализацию совместно используемого базового класса ZooAnimal. Даже при том, что класс ZooAnimal не является прямым базовым классом для класса Panda, часть ZooAnimal инициализирует конструктор класса Panda:
Panda::Panda(std::string name, bool onExhibit)
: ZooAnimal(name, onExhibit, "Panda"),
Bear(name, onExhibit),
Raccoon(name, onExhibit),
Endangered(Endangered::critical),
sleeping_flag(false) { }
Как создается объект при виртуальном наследованииПорядок создания объекта с виртуальным базовым классом немного отличается от обычного: сначала инициализируется часть виртуального базового класса с использованием инициализаторов, предоставленных в конструкторе для наиболее производного класса. Как только создана часть виртуального базового класса, создаются части прямых базовых классов в порядке их расположения в списке наследования.
Например, объект класса Panda создается так.
• Сначала создается часть виртуального базового класса ZooAnimal. При этом используются инициализаторы из списка инициализации конструктора класса Panda.
• Затем создается часть Bear.
• Затем создается часть Raccoon.
• Следующей создается часть прямого базового класса Endangered.
• Наконец создается часть Panda.
Если конструктор класса Panda не инициализирует явно часть базового класса ZooAnimal, будет использован стандартный конструктор класса ZooAnimal. Если у класса ZooAnimal нет стандартного конструктора, произойдет ошибка.
Части виртуальных базовых классов всегда создаются до частей обычных базовых классов, независимо от того, где они располагаются в иерархии наследования.
Порядок выполнения конструкторов и деструкторовУ класса может быть несколько виртуальных базовых классов. В этом случае части виртуальных классов создаются в порядке их расположения в списке наследования. Например, в следующей иерархии наследования у класса TeddyBear (МедвежонокТедди) есть два виртуальных базовых класса: прямой виртуальный базовый класс ToyAnimal (ИгрушечноеЖивотное) и косвенный базовый класс ZooAnimal, от которого происходит класс Bear:
class Character { /* ... */ };
class BookCharacter : public Character { /* ... */ };
class ToyAnimal { /* ... */ };
class TeddyBear : public BookCharacter,
public Bear, public virtual ToyAnimal
{ / * ... * / };
Чтобы выявить наличие виртуальных базовых классов, прямые базовые классы просматриваются в порядке объявления. Если это так, то сначала создаются части виртуальных базовых классов, затем выполняются конструкторы обычных, не виртуальных базовых классов в порядке их объявления. Таким образом, чтобы создать объект класса TeddyBear, конструкторы его частей вызываются в следующем порядке:
ZooAnimal(); // виртуальный базовый класс Bear
ToyAnimal(); // прямой виртуальный базовый класс
Character(); // косвенный базовый класс первого не виртуального
// базового класса
BookCharacter(); // первый прямой не виртуальный базовый класс
Bear(); // второй прямой не виртуальный базовый класс
TeddyBear(); // наиболее производный класс
Тот же порядок создания используется в синтезируемом конструкторе копий и конструкторах перемещения, в синтезируемых операторах присвоения члены присваиваются в том же порядке. Вызов деструкторов базовых классов осуществляется в порядке, обратном порядку вызова конструкторов. Часть TeddyBear будет удалена сначала, а часть ZooAnimal — последней.
Упражнения раздела 18.3.5Упражнение 18.29. Имеется следующая иерархия классов:
class Class { ... };
class Base : public Class { ... };
class D1 : virtual public Base { ... };
class D2 : virtual public Base { ... };
class MI : public D1, public D2 { ... };
class Final : public MI, public Class { ... };
(a) Каков порядок вызова конструкторов и деструкторов объектов класса Final?
(b) Сколько внутренних объектов класса Base находится в объекте класса Final? А сколько внутренних объектов класса Class?
(c) Какие из следующих случаев присвоения приведут к ошибке во время компиляции?
Base *pb; Class *pc; MI *pmi; D2 *pd2;
(a) pb = new Class; (b) pc = new Final;
(c) pmi = pb; (d) pd2 = pmi;
Упражнение 18.30. Определите в классе Base стандартный конструктор, конструктор копий и конструктор с параметром типа int. Определите те же три конструктора в каждом производном классе. Каждый конструктор должен использовать свой аргумент для инициализации своей части Base.
Резюме
Язык С++ применяется для решения широкого диапазона проблем: от требующих лишь нескольких часов работы до занимающих годы работы больших групп разработчиков. Некоторые из средств языка С++ наиболее полезны при создании крупномасштабных приложений. Имеется в виду обработка исключений, пространства имен и множественное или виртуальное наследование.
Обработка исключений позволяет отделить ту часть кода, где может произойти ошибка, от той части кода, где она обрабатывается. При передаче исключения выполнение текущей функции приостанавливается и начинается поиск ближайшей директивы catch. Локальные переменные, определенные в покидаемых при поиске директив catch функциях, удаляются в ходе обработки исключения.
Пространства имен — это механизм управления большими и сложными приложениями, формируемыми из кода, созданного независимыми поставщиками. Пространство имен является областью видимости, в которой могут быть определены объекты, типы, функции, шаблоны и другие пространства имен. Стандартная библиотека определена в пространстве имен std.
С концептуальной точки зрения множественное наследование — довольно простое понятие: производный класс может быть унаследован от нескольких прямых базовых классов. Объект производного класса состоит из частей, представляющих собой внутренние объекты всех своих базовых классов. Концепция действительно проста, но на практике сопряжена со многими сложностями. В частности, наследование от нескольких базовых классов создает вероятность конфликтов имен и в результате порождает неоднозначные обращения к именам из базовых частей объекта.
Если класс происходит от нескольких непосредственных базовых классов, не исключена ситуация, когда эти классы сами могут иметь общий базовый класс. В таких случаях промежуточные классы могут применить виртуальное наследование, позволяющее другим классам иерархии, унаследовавшим тот же базовый класс, совместно использовать его внутренний объект. Таким образом, объект производного класса будет иметь только одну совместно используемую копию внутреннего объекта виртуального базового класса.