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

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

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

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

962

BookCharacter Bear

TeddyBear

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

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

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

Непосредственные базовые классы просматриваются в порядке их объявления при поиске среди них виртуальных. В нашем примере сначала анализируется поддерево наследования BookCharacter, затем Bear и наконец ToyAnimal. Каждое поддерево обходится в глубину, т.е. поиск начинается с корневого класса и продвигается вниз. Так, для поддерева BookCharacter сначала просматривается Character, а затем

BookCharacter. Для поддерева Bear ZooAnimal, а потом Bear.

При описанном алгоритме поиска порядок вызова конструкторов виртуальных базовых классов для TeddyBear таков: ZooAnimal, потом ToyAnimal.

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

Если имеется объявление:

TeddyBear Paddington;

ZooAnimal();

// виртуальный базовый класс Bear

ToyAnimal();

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

Character();

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

BookCharacter();

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

Bear();

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

то последовательность вызова конструкторов базовых классов будет такой:

TeddyBear();

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

причем за инициализацию ZooAnimal и ToyAnimal отвечает TeddyBear ближайший производный класс объекта Paddington.

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

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

963

18.5.4. Видимость членов виртуального базового класса

Изменим наш класс Bear так, чтобы он имел собственную реализацию функции-члена onExhibit(), предоставляемой также ZooAnimal:

bool Bear::onExhibit() { ... }

Теперь обращение к onExhibit() через объект Bear разрешается в пользу экземпляра,

Bear winnie( "любитель меда" );

определенного в этом классе:

winnie.onExhibit(); // Bear::onExhibit()

Обращение же к onExhibit() через объект Raccoon разрешается в пользу функции-

Raccoon meeko( "любитель всякой еды" );

члена, унаследованной из ZooAnimal:

meeko.onExhibit();

// ZooAnimal::onExhibit()

Производный класс Panda наследует члены своих базовых классов. Их можно отнести к одной из трех категорий:

члены виртуального базового класса ZooAnimal, такие, как name() и family(), не замещенные ни в Bear, ни в Raccoon;

член onExhibit() виртуального базового класса ZooAnimal, наследуемый при обращении через Raccoon и замещенный в классе Bear;

специализированные в классах Bear и Raccoon экземпляры функции print() из

ZooAnimal.

Можно ли, не опасаясь неоднозначности, напрямую обращаться к унаследованным членам из области видимости класса Panda? В случае невиртуального наследования нет: все неквалифицированные ссылки на имя неоднозначны. Что касается виртуального наследования, то прямое обращение допустимо к любым членам из первой и второй категорий. Например, дан объект класса Panda:

Panda spot( "Spottie" );

Тогда инструкция

spot.name();

вызывает разделяемую функцию-член name() виртуального базового ZooAnimal, а

инструкция

spot.onExhibit();

вызывает функцию-член onExhibit() производного класса Bear.

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

964

Когда два или более экземпляров члена наследуются разными путями (это относится не только к функциям-членам, но и к данным-членам, а также к вложенным типам) и все они представляют один и тот же член виртуального базового класса, неоднозначности не возникает, поскольку существует единственный разделяемый экземпляр (первая категория). Если один экземпляр представляет член виртуального базового, а другой член унаследованного от него класса, то неоднозначности также не возникает:

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

Например, при невиртуальном наследовании неквалифицированное обращение к

// ошибка: неоднозначно при невиртуальном наследовании Panda yolo( "любитель бамбука" );

onExhibit() через объект Panda неоднозначно: yolo.onExhibit();

В данном случае все унаследованные экземпляры имеют равные приоритеты при разрешении имени, поэтому неквалифицированное обращение приводит к ошибке компиляции из-за неоднозначности (см. раздел 18.4.1).

При виртуальном наследовании члену, унаследованному из виртуального базового класса, приписывается меньший приоритет, чем члену с тем же именем, замещенному в производном. Так, унаследованному от Bear экземпляру onExhibit() отдается

//правильно: при виртуальном наследовании неоднозначности нет

//вызывается Bear::onExhibit()

предпочтение перед экземпляром из ZooAnimal, унаследованному через Raccoon: yolo.onExhibit();

Если два или более классов на одном и том же уровне наследования замещают некоторый член виртуального базового, то в производном они будут иметь одинаковый вес. Например, если в Raccoon также определен член onExhibit(), то при обращении к нему

из Panda придется квалифицировать имя с помощью оператора разрешения области

bool Panda::onExhibit()

{

return Bear::onExhibit() && Raccoon::onExhibit() && ! _sleeping;

видимости:

}

Упражнение 18.13 Дана иерархия классов:

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

965

 

class Class { ... };

 

 

 

 

class Base : public Class { ... };

 

 

class Derived1 : virtual public Base { ... };

 

 

class Derived2 : virtual public Base { ... };

 

 

class MI : public Derived1,

 

 

public Derived2 { ... };

 

 

class Final : public MI, public Class { ... };

 

 

 

 

 

 

(a)В каком порядке вызываются конструкторы и деструкторы при определении объекта

Final?

(b)Сколько подобъектов класса Base содержит объект Final? А сколько подобъектов

Class?

Base

*pb;

MI

*pmi;

Class

*pc;

Derived2 *pd2;

(i) pb = new Class; (iii) pmi = pb;

(c) Какие из следующих присваиваний вызывают ошибку компиляции?

(ii) pc = new Final; (iv) pd2 = pmi;

Упражнение 18.14

Дана иерархия классов:

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

966

class Base { public:

bar( int );

//...

protected: int ival;

//...

};

class Derived1 : virtual public Base { public:

bar( char ); foo( char );

//...

protected: char cval;

//...

};

class Derived2 : virtual public Base { public:

foo( int ); // ...

protected: int ival; char cval; // ...

};

class VMI : public Derived1, public Derived2 {};

К каким из унаследованных членов можно обращаться из класса VMI, не квалифицируя имя? А какие требуют квалификации?

Упражнение 18.15

class Base { public:

Base();

Base( string ); Base( const Base& ); // ...

protected: string _name;

Дан класс Base с тремя конструкторами:

};

(a)любой из

class Derived1 : virtual public Vase { ... }; class Derived2 : virtual public Vase { ... };

(b)class VMI : public Derived1, public Derived2 { ... };

Определите соответствующие конструкторы для каждого из следующих классов:

(c) class Final : public VMI { ... };

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

 

967

18.6.

Пример

множественного

виртуального

наследования

A

 

 

 

Мы продемонстрируем определение и использование множественного виртуального наследования, реализовав иерархию шаблонов классов Array (см. раздел 2.4) на основе шаблона Array (см. главу 16), модифицированного так, чтобы он стал конкретным базовым классом. Перед тем как приступать к реализации, поговорим о взаимосвязях между шаблонами классов и наследованием.

Конкретизированный экземпляр такого шаблона может выступать в роли явного базового класса:

class IntStack : private Array<int> {};

class Base {}; template <class Type>

Разрешается также произвести его от не шаблонного базового класса: class Derived : public Base {};

template <class Type>

Шаблон может выступать одновременно в роли базового и производного классов: class Array_RC : public virtual Array<Type> {};

В первом примере конкретизированный типом int шаблон Array служит закрытым базовым классом для не шаблонного IntStack. Во втором примере не шаблонный Base служит базовым для любого класса, конкретизированного из шаблона Derived. В

третьем примере любой конкретизированный из шаблона Array_RC класс является производным от класса, конкретизированного из шаблона Array. Так, инструкция

Array_RC<int> ia;

конкретизирует экземпляры шаблонов Array и Array_RC.

template < typename Type >

Кроме того, сам параметр-шаблон может служить базовым классом [MURRAY93]: class Persistent : public Type { ... };

в данном примере определяется производный устойчивый (persistent) подтип для любого конкретизированного типа. Как отмечает Мюррей (Murray), на Type налагается неявное ограничение: он должен быть типом класса. Например, инструкция

Persistent< int > pi;

// ошибка

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

968

приводит к ошибке компиляции, поскольку встроенный тип не может быть объектом наследования.

Шаблон, выступающий в роли базового класса, должен квалифицироваться полным списком параметров. Если имеется определение:

template <class T> class Base {};

template < class Type >

то необходимо писать:

class Derived : public Base<Type> {};

//ошибка: Base - это шаблон,

//так что должны быть заданы его аргументы

template < class Type >

Такая запись неправильна:

class Derived : public Base {};

В следующем разделе шаблон Array, определенный в главе 16, выступает в роли виртуального базового класса для подтипа Array, контролирующего выход за границы массива; для отсортированного подтипа Array; для подтипа Array, который обладает обоими указанными свойствами. Однако первоначальное определение шаблона класса Array для наследования не подходит:

все его члены и вспомогательные функции объявлены закрытыми, а не защищенными;

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

Означает ли это, что наша первоначальная реализация была неправильной? Нет. Она была верной на том уровне понимания, которым мы тогда обладали. При реализации

шаблона класса Array мы еще не осознали необходимость специализированных подтипов. Теперь, однако, определение шаблона придется изменить так (реализации функций-членов при этом останутся теми же):

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

969

#ifndef ARRAY_H #define ARRAY_H

#include <iostream>

// необходимо для опережающего объявления operator<< template <class Type> class Array;

template <class Type> ostream&

operator<<( ostream &, Array<Type> & );

template <class Type> class Array {

static const int ArraySize = 12; public:

explicit Array( int sz = ArraySize )

{ init( 0, sz ); }

Array( const Type *ar, int sz )

{ init( ar, sz ); }

Array( const Array &iA )

{ init( iA.ia, iA.size()); }

virtual ~Array()

{ delete[] ia; }

Array& operator=( const Array & ); int size() const { return _size; } virtual void grow();

virtual void print( ostream& = cout );

Type at( int ix ) const { return ia[ ix ]; }

virtual Type& operator[]( int ix ) { return ia[ix]; }

virtual void sort( int,int ); virtual int find( Type ); virtual Type min();

virtual Type max();

protected:

void swap( int, int );

void init( const Type*, int ); int _size;

Type *ia;

};

#endif

Одна из проблем, связанных с таким переходом к полиморфизму, заключается в том, что

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

int find( const Array< int > &ia, int value )

{

for ( int ix = 0; ix < ia.size(); ++ix ) // а теперь вызов виртуальной функции if ( ia[ ix ] == value )

return ix;

return -1;

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

}

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

970

Для повышения производительности мы включили встроенную функцию-член at(),обеспечивающую прямое чтение элемента.

18.6.1. Порождение класса, контролирующего выход за границы массива

В функции try_array() из раздела 16.13, предназначенной для тестирования нашей

int index = iA.find( find_val );

предыдущей реализации шаблона класса Array, есть две инструкции:

Type value = iA[ index ];

find() возвращает индекс первого вхождения значения find_val или -1, если значение в массиве не найдено. Этот код некорректен, поскольку в нем не проверяется, что не была возвращена -1. Поскольку -1 находится за границей массива, то каждая инициализация value может привести к ошибке. Поэтому мы создадим подтип Array, который будет контролировать выход за границы массива, – Array_RC и поместим его определение в

#ifndef ARRAY_RC_H #define ARRAY_RC_H

#include "Array.h"

template <class Type>

class Array_RC : public virtual Array<Type> { public:

Array_RC( int sz = ArraySize ) : Array<Type>( sz ) {} Array_RC( const Array_RC& r );

Array_RC( const Type *ar, int sz ); Type& operator[]( int ix );

};

заголовочный файл Array_RC.h:

#endif

Внутри определения производного класса каждая ссылка на спецификатор типа шаблона

Array_RC( int sz = ArraySize )

базового должна быть квалифицирована списком формальных параметров:

:Array<Type>( sz ) {}

//ошибка: Array - это не спецификатор типа

Такая запись неправильна:

Array_RC( int sz = ArraySize ) : Array( sz ) {}

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

971

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

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

#include "Array_RC.h" #include "Array.C" #include <assert.h>

template <class Type>

Array_RC<Type>::Array_RC( const Array_RC<Type> &r )

:Array<Type>( r ) {}

template <class Type>

Array_RC<Type>::Array_RC( const Type *ar, int sz )

:Array<Type>( ar, sz ) {}

template <class Type>

Type &Array_RC<Type>::operator[]( int ix ) {

assert( ix >= 0 && ix < Array<Type>::_size ); return ia[ ix ];

16.18):

}

Мы квалифицировали обращения к членам базового класса Array, например к _size, чтобы предотвратить просмотр Array до момента конкретизации шаблона:

Array<Type>::_size;

Мы достигаем этого, включая в обращение параметр шаблона. Таким образом, имена в определении Array_RC разрешаются тогда, когда определяется шаблон (за исключением имен, явно зависящих от его параметра). Если встречается неквалифицированное имя _size, то компилятор должен найти его определение, если только это имя не зависит явно от параметра шаблона. Мы сделали имя _size зависящим от параметра шаблона, предварив его именем базового класса Array<Type>. Теперь компилятор не будет пытаться разрешить имя _size до момента конкретизации шаблона. (В определении класса Array_Sort мы приведем другие примеры использования подобных приемов.)

Каждая конкретизация Array_RC порождает экземпляр класса Array. Например:

Array_RC<string> sa;

конкретизирует параметром string как шаблон Array_RC, так и шаблон Array. Приведенная ниже программа вызывает try_array() (реализацию см. в разделе 16.13), передавая ей объекты подтипа Array_RC. Если все сделано правильно, то выходы за границы массивы будут замечены: