Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
C++ для начинающих.pdf
Скачиваний:
183
Добавлен:
01.05.2014
Размер:
3.97 Mб
Скачать

Рис. 18.2. Иерархия виртуального наследования iostream (упрощенная)

Еще один реальный пример виртуального и множественного наследования дают распределенные объектные вычисления. Подробное рассмотрение этой темы см. в серии статей Дугласа Шмидта (Douglas Schmidt) и Стива Виноски (Steve Vinoski) в [LIPPMAN96b].

В данной главе мы рассмотрим использование и поведение механизмов виртуального и множественного наследования. В другой нашей книге, “Inside the C++ Object Model”, описаны более сложные вопросы производительности и дизайна этого аспекта языка.

Для последующего обсуждения мы выбрали иерархию животных в зоопарке. Наши животные существуют на разных уровнях абстракции. Есть, конечно, особи, имеющие свои имена: Линь-Линь, Маугли или Балу. Каждое животное принадлежит к какому-то виду; скажем, Линь-Линь – это гигантская панда. Виды в свою очередь входят в семейства. Так, гигантская панда – член семейства медведей, хотя, как мы увидим в разделе 18.5, по этому поводу в зоологии долго велись бурные дискуссии. Каждое семейство – член животного мира, в нашем случае ограниченного территорией зоопарка.

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

Помимо классов, описывающих животных, есть и вспомогательные классы, инкапсулирующие различные абстракции иного рода, например “животные, находящиеся под угрозой вымирания”. Наша реализация класса Panda множественно наследует от Bear (медведь) и Endangered (вымирающие).

18.2. Множественное наследование

Для поддержки множественного наследования синтаксис списка базовых классов

class Bear : public ZooAnimal { ... };

расширяется: допускается наличие нескольких базовых классов, разделенных запятыми:

class Panda : public Bear, public Endangered { ... };

Для каждого из перечисленных базовых классов должен быть указан уровень доступа: public, protected или private. Как и при одиночном наследовании, множественно наследовать можно только классу, определение которого уже встречалось ранее.

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

В случае множественного наследования объект производного класса содержит по одному подобъекту каждого из своих базовых (см. раздел 17.3). Например, когда мы пишем

Panda ying_yang;

то объект ying_yang будет состоять из подобъекта класса Bear (который в свою очередь содержит подобъект ZooAnimal), подобъекта Endangered и нестатических членов, объявленных в самом классе Panda, если таковые есть (см. рис. 18.3).

ZooAnimal

Endangered

Bear

Panda

Рис. 18.3. Иерархия множественного наследования класса Panda

Конструкторы базовых классов вызываются в порядке объявления в списке базовых классов. Например, для ying_yang эта последовательность такова: конструктор Bear (но поскольку класс Bear – производный от ZooAnimal, то сначала вызывается конструктор ZooAnimal), затем конструктор Endangered и в самом конце конструктор Panda.

Как отмечалось в разделе 17.4, на порядок вызова не влияет ни наличие базовых классов в списке инициализации членов, ни порядок их перечисления. Иными словами, если бы конструктор Bear вызывался неявно и потому не был бы упомянут в списке

//конструктор по умолчанию класса Bear вызывается до

//конструктора класса Endangered с двумя аргументами ...

Panda::Panda()

: Endangered( Endangered::environment,

Endangered::critical )

инициализации членов, как в следующем примере:

{ ... }

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

Порядок вызова деструкторов всегда противоположен порядку вызова конструкторов. В нашем примере деструкторы вызываются в такой последовательности: ~Panda(),

~Endangered(), ~Bear(), ~ZooAnimal().

В разделе 17.3 уже говорилось, что в случае одиночного наследования к открытым и защищенным членам базового класса можно обращаться напрямую (не квалифицируя имя члена именем его класса), как если бы они были членами производного класса. То же самое справедливо и для множественного наследования. Однако при этом можно унаследовать одноименные члены из двух или более базовых классов. В таком случае прямое обращение оказывается неоднозначным и приводит к ошибке компиляции.

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

фактического обращения к нему (см. раздел 17.4). Например, если в обоих классах Bear и Endangered определена функция-член print(), то инструкция

ying_yang.print( cout );

приводит к ошибке компиляции, даже если у двух унаследованных функций-членов

Error: ying_yang.print( cout ) -- ambiguous, one of

Bear::print( ostream& )

разные списки параметров.

Ошибка: ying_yang.print( cout ) -- неоднозначно, одна из

Bear::print( ostream& )

Endangered::print( ostream&, int ) Endangered::print( ostream&, int )

Причина в том, что унаследованные функции-члены не образуют множество перегруженных функций внутри производного класса (см. раздел 17.3). Поэтому print() разрешается только по имени, а не по типам фактических аргументов. (О том, как производится разрешение, мы поговорим в разделе 18.4.)

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

extern void display( const Bear& );

extern void highlight( const Endangered& ); Panda ying_yang;

display( ying_yang ); // правильно highlight( ying_yang ); // правильно

extern ostream&

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

можно преобразовать в указатель, ссылку или объект ZooAnimal, Bear или Endangered: cout << ying_yang << endl; // правильно

Однако вероятность неоднозначных преобразований при множественном наследовании

extern void display( const Bear& );

намного выше. Рассмотрим, к примеру, две функции: extern void display( const Endangered& );

Неквалифицированный вызов display() для объекта класса Panda

Panda

ying _yan g;

display( ying_yang ); // ошибка: неоднозначность

приводит к ошибке компиляции:

Error: display( ying_yang ) -- ambiguous, one of display( const Bear& );

display( const Endangered& );

Ошибка: display( ying_yang ) -- неоднозначно, одна из display( const Bear& );

display( const Endangered& );

Компилятор не может различить два непосредственных базовых класса с точки зрения преобразования производного. Равным образом применимы обе трансформации. (Мы покажем способ разрешения этого конфликта в разделе 18.4.)

Чтобы понять, какое влияние оказывает множественное наследование на механизм виртуальных функций, определим их набор в каждом из непосредственных базовых классов Panda. (Виртуальные функции введены в разделе 17.2 и подробно обсуждались в

class Bear : public ZooAnimal { public:

virtual ~Bear();

virtual ostream& print( ostream& ) const;

virtual string isA() const; // ...

};

class Endangered { public:

virtual ~Endangered();

virtual ostream& print( ostream& ) const;

virtual void highlight() const; // ...

разделе 17.5.)

};

Теперь определим в классе Panda собственный экземпляр print(), собственный деструктор и еще одну виртуальную функцию cuddle():

class Panda : public Bear, public Endangered

{

public:

virtual ~Panda();

virtual ostream& print( ostream& ) const;

virtual void cuddle(); // ...

};

Множество виртуальных функций, которые можно напрямую вызывать для объекта Panda, представлено в табл. 18.1.

Таблица 18.1. Виртуальные функции для класса Panda

 

деструктор

 

 

Panda::~Panda()

 

 

 

 

 

print(ostream&) const

 

 

Panda::print(ostream&)

 

 

 

 

 

 

isA() const

 

 

Bear::isA()

 

 

 

 

 

 

highlight() const

 

 

Endangered::highlight()

 

 

 

 

 

 

cuddle()

 

 

Panda::cuddle()

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Имя виртуальной функции

Активный экземпляр

 

 

 

 

 

 

Когда ссылка или указатель на объект Bear или ZooAnimal инициализируется адресом объекта Panda или ему присваивается такой адрес, то части интерфейса, связанные с классами Panda и Endangered, становятся недоступны:

Bear *pb = new Panda;

 

pb->print( cout );

// правильно: Panda::print(ostream&)

pb->isA();

// правильно: Bear::isA()

pb->cuddle();

// ошибка: это не часть интерфейса

Bear

// ошибка: это не часть интерфейса

pb->highlight();

Bear

 

delete pb;

// правильно: Panda::~Panda()

(Обратите внимание, что если бы объекту класса Panda был присвоен указатель на ZooAnimal, то все показанные выше вызовы разрешались бы так же.)

Аналогично, если ссылка или указатель на объект Endangered инициализируется адресом объекта Panda или ему присваивается такой адрес, то части интерфейса,

Endangered *pe = new Panda;

pe->print( cout ); // правильно: Panda::print(ostream&)

// ошибка: это не часть интерфейса Endangered pe->cuddle();

pe->highlight();

// правильно:

Endangered::highlight()

связанные с классами Panda и Bear, становятся недоступными:

delete pe;

// правильно: Panda::~Panda()

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

//ZooAnimal *pz = new Panda; delete pz;

//Bear *pb = new Panda; delete pb;

//Panda *pp = new Panda; delete pp;

//Endangered *pe = new Panda;

конструкторов: delete pe;

Деструктор класса Panda вызывается с помощью механизма виртуализации. После его выполнения по очереди статически вызываются деструкторы Endangered и Bear, а в самом конце – ZooAnimal.

Почленная инициализация и присваивание объекту производного класса, наследующего нескольким базовым, ведут себя точно так же, как и при одиночном наследовании (см. раздел 17.6). Например, для нашего объявления класса Panda

class Panda : public Bear, public Endangered

{ ... };

Panda

yin _ya ng;

в результате почленной инициализации объекта ling_ling

Panda ling_ling = yin_yang;

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

Упражнение 18.1 Какие из следующих объявлений ошибочны? Почему?

(a)class CADVehicle : public CAD, Vehicle { ... };

(b)class

DoublyLinkedLis t:

public List, public List { ... };

(c) class

iostrea m:

private istream, private ostream { ... };

Упражнение 18.2

class A { ... };

class B : public A { ... }; class C : public B { ... }; class X { ... };

class Y { ... };

class Z : public X, public Y { ... };

Дана иерархия, в каждом классе которой определен конструктор по умолчанию: class MI : public C, public Z { ... };

Каков порядок вызова конструкторов в таком определении:

MI mi;

Упражнение 18.3

class X { ... }; class A { ... };

class B : public A { ... }; class C : private B

{ ... };

Дана иерархия, в каждом классе которой определен конструктор по умолчанию: class D : public X, public C { ... };

Какие из следующих преобразований недопустимы:

D *pd = new D;

(a)

X

*px

=

pd;

(c)

B

*pb

=

pd;

(b)

A

*pa

=

pd;

(d)

C

*pc

=

pd;

Упражнение 18.4

class Base { public:

virtual ~Base();

virtual ostream& print(); virtual void debug(); virtual void readOn(); virtual void writeOn(); // ...

};

class Derived1 : virtual public Base { public:

virtual ~Derived1(); virtual void writeOn(); // ...

};

class Derived2 : virtual public Base { public:

virtual ~Derived2(); virtual void readOn(); // ...

};

class MI : public Derived1, public Derived2

{

public:

virtual ~MI();

virtual ostream& print(); virtual void debug();

// ...

Дана иерархия классов, обладающая приведенным ниже набором виртуальных функций:

};