Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Программирование на C / C++ / Ален И. Голуб. Правила программирования на Си и Си++ [pdf]

.pdf
Скачиваний:
138
Добавлен:
02.05.2014
Размер:
5.67 Mб
Скачать

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

952

class Base1 { public:

// ...

protected:

int ival; double dval; char cval; // ...

private:

int *id; // ...

};

class Base2 { public:

//...

protected: float fval;

//...

private: double dval; // ...

};

class Derived : public Base1 { public:

//...

protected: string sval; double dval;

//...

};

class MI : public Derived, public Base2 { public:

//...

protected:

int *ival; complex<double> cval;

//...

};

int ival; double dval;

void MI::

foo( double dval )

{

int id;

//...

иструктура функции-члена MI::foo():

}

(a)Какие члены видны в классе MI? Есть ли среди них такие, которые видны в нескольких базовых?

(b)Какие члены видны в MI::foo()?

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

953

Упражнение 18.10

Пользуясь иерархией классов из упражнения 18.9, укажите, какие из следующих

void MI:: bar()

{

int sval;

// вопрос упражнения относится к коду, начинающемуся с этого места ...

}

(a) dval = 3.14159; (d) fval = 0;

(b) cval = 'a'; (e) sval = *ival;

присваиваний недопустимы внутри функции-члена MI::bar():

(c) id = 1;

Упражнение 18.11

int id;

void MI::

foobar( float cval )

{

int dval;

// вопросы упражнения относятся к коду, начинающемуся с этого места ...

Даны иерархия классов из упражнения 18.9 и скелет функции-члена MI::foobar():

}

(a)Присвойте локальной переменной dval сумму значений члена dval класса Base1 и члена dval класса Derived.

(b)Присвойте вещественную часть члена cval класса MI члену fval класса Base2.

(c)Присвойте значение члена cval класса Base1 первому символу члена sval класса

Derived.

Упражнение 18.12

Дана следующая иерархия классов, в которых имеются функции-члены print():

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

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:

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

958

class Bear : public virtual ZooAnimal {

 

public:

 

enum DanceType {

 

two_left_feet, macarena, fandango, waltz };

 

Bear( string name, bool onExhibit=true )

:ZooAnimal( name, onExhibit, "Bear" ), _dance( two_left_feet )

{}

virtual ostream& print( ostream& ) const; void dance( DanceType );

// ...

protected: DanceType _dance; // ...

};

class Raccoon : public virtual ZooAnimal { public:

Raccoon( string name, bool onExhibit=true )

:ZooAnimal( name, onExhibit, "Raccoon" ), _pettable( false )

{}

virtual ostream& print( ostream& ) const;

bool pettable() const { return _pettable; }

void pettable( bool petval ) { _pettable = petval; } // ...

protected:

bool _pettable;

//...

Авот объявление класса Raccoon:

};

18.5.2. Специальная семантика инициализации

Наследование, в котором присутствует один или несколько виртуальных базовых классов, требует специальной семантики инициализации. Взгляните еще раз на реализации Bear и Raccoon в предыдущем разделе. Видите ли вы, какая проблема связана с порождением класса Panda?

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

959

class Panda : public Bear,

public Raccoon, public Endangered {

public:

Panda( string name, bool onExhibit=true ); virtual ostream& print( ostream& ) const;

bool sleeping() const { return _sleeping; }

void sleeping( bool newval ) { _sleeping = newval; } // ...

protected:

bool _sleeping; // ...

};

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

В случае невиртуального наследования производный класс способен явно инициализировать только свои непосредственные базовые классы (см. раздел 17.4). Так, классу Panda, наследующему от ZooAnimal, не разрешается напрямую вызвать конструктор ZooAnimal в своем списке инициализации членов. Однако при виртуальном

наследовании только Panda может напрямую вызывать конструктор своего виртуального базового класса ZooAnimal.

Ответственность за инициализацию виртуального базового возлагается на ближайший производный класс. Например, когда объявляется объект класса Bear:

Bear winnie( "pooh" );

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

cout << winnie.family_name();

будет выведена строка:

The family name for pooh is Bear

(Название семейства для pooh – это Bear)

Аналогично для объявления

Raccoon meeko( "meeko" );

Raccoon это ближайший производный класс для объекта meeko, поэтому выполняется вызов конструктора ZooAnimal, определенный в классе Raccoon. Когда мы пишем:

cout << meeko.family_name();

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

960

печатается строка:

The family name for meeko is Raccoon

(Название семейства для meeko - это Raccoon)

Если же объявить объект типа Panda:

Panda yolo( "yolo" );

то ближайшим производным классом для объекта yolo будет Panda, поэтому он и отвечает за инициализацию ZooAnimal.

Когда инициализируется объект Panda, то явные вызовы конструктора ZooAnimal в конструкторах классов Raccoon и Bear не выполняются, а вызывается он с теми аргументами, которые указаны в списке инициализации членов объекта Panda. Вот так

Panda::Panda( string name, bool onExhibit=true )

:ZooAnimal( name, onExhibit, "Panda" ), Bear( name, onExhibit ),

Raccoon( name, onExhibit ), Endangered( Endangered::environment,

Endangered::critical ), sleeping( false )

выглядит реализация:

{}

Если в конструкторе Panda аргументы для конструктора ZooAnimal не указаны явно, то вызывается конструктор ZooAnimal по умолчанию либо, если такового нет, выдается ошибка при компиляции определения конструктора Panda.

Когда мы пишем:

cout << yolo.family_name();

печатается строка:

The family name for yolo is Panda

(Название семейства для yolo - это Panda)

Внутри определения Panda классы Raccoon и Bear являются промежуточными, а не ближайшими производными. В промежуточном производном классе все прямые вызовы конструкторов виртуальных базовых классов автоматически подавляются. Если бы от Panda был в дальнейшем произведен еще один класс, то сам класс Panda стал бы промежуточным и вызов из него конструктора ZooAnimal также был бы подавлен.

Обратите внимание, что оба аргумента, передаваемые конструкторам Bear и Raccoon, излишни в том случае, когда они выступают в роли промежуточных производных классов. Чтобы избежать передачи ненужных аргументов, мы можем предоставить явный конструктор, вызываемый, когда класс оказывается промежуточным производным. Изменим наш конструктор Bear:

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

961

class Bear : public virtual ZooAnimal { public:

// если выступает в роли ближайшего производного класса

Bear( string name, bool onExhibit=true )

:ZooAnimal( name, onExhibit, "Bear" ), _dance( two_left_feet )

{}

// ... остальное без изменения

protected:

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

Bear() : _dance( two_left_feet ) {}

//... остальное без изменения

};

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

Panda::Panda( string name, bool onExhibit=true )

:ZooAnimal( name, onExhibit, "Panda" ), Endangered( Endangered::environment,

Endangered::critical ), sleeping( false )

класса Raccoon, можно следующим образом модифицировать конструктор Panda:

{}

18.5.3. Порядок вызова конструкторов и деструкторов

Виртуальные базовые классы всегда конструируются перед невиртуальными, вне зависимости от их расположения в иерархии наследования. Например, в приведенной иерархии у класса TeddyBear (плюшевый мишка) есть два виртуальных базовых: непосредственный ToyAnimal (игрушечное животное) и экземпляр ZooAnimal, от

class Character { ... };

// персонаж

class BookCharacter : public Character { ... };

class ToyAnimal { ... };

// литературный персонаж

// игрушка

class TeddyBear : public BookCharacter,

public Bear, public virtual ToyAnimal

которого унаследован класс Bear:

{ ... };

Эта иерархия изображена на рис. 18.5, где виртуальное наследование показано пунктирной стрелкой, а невиртуальное сплошной.

Character

ZooAnimal

ToyAnimal