Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
C++ для начинающих (Стенли Липпман) 3-е хххх.pdf
Скачиваний:
86
Добавлен:
30.05.2015
Размер:
5.92 Mб
Скачать

С++ для начинающих

954

class Base { public:

void print( string ) const; // ...

};

class Derived1 : public Base { public:

void print( int ) const; // ...

};

class Derived2 : public Base { public:

void print( double ) const; // ...

};

class MI : public Derived1, public Derived2 { public:

void print( complex<double> ) const; // ...

};

MI mi;

string dancer( "Nejinsky" );

(a)Почему приведенный фрагмент дает ошибку компиляции? mi.print( dancer );

(b)Как изменить определение MI, чтобы этот фрагмент компилировался и выполнялся правильно?

18.5. Виртуальное наследование A

По умолчанию наследование в C++ является специальной формой композиции по значению. Когда мы пишем:

class Bear : public ZooAnimal { ... };

каждый объект Bear содержит все нестатические данные-члены подобъекта своего базового класса ZooAnimal, а также нестатические члены, объявленные в самом Bear. Аналогично, если производный класс является базовым для какого-то другого:

class PolarBear : public Bear { ... };

то каждый объект PolarBear содержит все нестатические члены, объявленные в

PolarBear, Bear и ZooAnimal.

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

С++ для начинающих

955

известный реальный пример такого рода это иерархия классов iostream. Взгляните еще раз на рис. 18.2: istream и ostream наследуют одному и тому абстрактному базовому классу ios, а iostream является производным как от istream, так и от

class iostream :

ostream.

public istream, public ostream { ... };

По умолчанию каждый объект iostream содержит два подобъекта ios: из istream и из ostream. Почему это плохо? С точки зрения эффективности хранение двух копий подобъекта ios пустая трата памяти, поскольку объекту iostream нужен только один экземпляр. Кроме того, конструктор вызывается для каждого подобъекта. Более серьезной проблемой является неоднозначность, к которой приводит наличие двух экземпляров. Например, любое неквалифицированное обращение к члену класса ios дает ошибку компиляции. Какой экземпляр имеется в виду? Что будет, если классы istream и ostream инициализируют свои подобъекты ios по-разному? Можно ли гарантировать, что в классе iostream используется согласованная пара членов ios? Применяемый по умолчанию механизм композиции по значению не дает таких гарантий.

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

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

class Panda : public Bear,

Panda от обоих классов:

public Raccoon, public Endangered { ... };

Наша виртуальная иерархия наследования Panda показана на рис. 18.4: две пунктирные стрелки обозначают виртуальное наследование классов Bear и Raccoon от ZooAnimal, а три сплошные невиртуальное наследование Panda от Bear, Raccoon и, на всякий случай, от класса Endangered из раздела 18.2.

ZooAnimal

Endangered

Bear Raccoon

С++ для начинающих

956

 

 

 

Panda

¾¾> невиртуальное наследование

- - - -> виртуальное наследование

Рис. 18.4. Иерархия виртуального наследования класса Panda

На данном рисунке показан интуитивно неочевидный аспект виртуального наследования: оно (в нашем случае наследование классов Bear и Raccoon) должно появиться в иерархии раньше, чем в нем возникнет реальная необходимость. Необходимым виртуальное наследование становится только при объявлении класса Panda, но если перед этим базовые классы Bear и Raccoon не наследуют своему базовому виртуально, то проектировщику класса Panda не повезло.

Должны ли мы производить свои базовые классы виртуально просто потому, что где-то ниже в иерархии может потребоваться виртуальное наследование? Нет, это не рекомендуется: снижение производительности и усложнение дальнейшего наследования может оказаться существенным (см. [LIPPMAN96a], где приведены и обсуждаются результаты измерения производительности).

Когда же использовать виртуальное наследование? Чтобы его применение было успешным, иерархия, например библиотека iostream или наше дерево классов Panda, должна проектироваться целиком либо одним человеком, либо коллективом разработчиков.

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

18.5.1. Объявление виртуального базового класса

Для указания виртуального наследования в объявление базового класса вставляется модификатор virtual. Так, в данном примере ZooAnimal становится виртуальным

//взаимное расположение ключевых слов public и virtual

//несущественно

class Bear : public virtual ZooAnimal { ... };

базовым для Bear и Raccoon:

class Raccoon : virtual public ZooAnimal { ... };

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

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

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

С++ для начинающих

957

преобразования базовых классов Panda выполняются корректно, хотя Panda использует

extern void dance( const Bear* ); extern void rummage( const Raccoon* );

extern ostream&

operator<<( ostream&, const ZooAnimal& );

int main()

{

Panda yin_yang;

dance( &yin_yang ); // правильно rummage( &yin_yang ); // правильно

cout << yin_yang;

// правильно

// ...

 

виртуальное наследование:

}

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

#include <iostream> #include <string>

class ZooAnimal; extern ostream&

operator<<( ostream&, const ZooAnimal& ); class ZooAnimal {

public:

ZooAnimal( string name,

bool onExhibit, string fam_name )

:_name( name ),

_onExhibit( onExhibit ), _fam_name( fam_name )

{}

virtual ~ZooAnimal();

virtual ostream& print( ostream& ) const; string name() const { return _name; }

string family_name() const { return _fam_name; } // ...

protected:

bool _onExhibit; string _name; string _fam_name; // ...

классы. Так выглядит объявление ZooAnimal:

};

К объявлению и реализации непосредственного базового класса при использовании виртуального наследования добавляется ключевое слово virtual. Вот, например, объявление нашего класса Bear: