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

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

930

свои имена: Линь-Линь, Маугли или Балу. Каждое животное принадлежит к какому-то виду; скажем, Линь-Линь это гигантская панда. Виды в свою очередь входят в семейства. Так, гигантская панда член семейства медведей, хотя, как мы увидим в разделе 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

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

931

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& )

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

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

932

Ошибка: 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& );

Panda ying_yang;

Неквалифицированный вызов display() для объекта класса Panda 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& );

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

933

Компилятор не может различить два непосредственных базовых класса с точки зрения преобразования производного. Равным образом применимы обе трансформации. (Мы покажем способ разрешения этого конфликта в разделе 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(), собственный

class Panda : public Bear, public Endangered

{

public:

virtual ~Panda();

virtual ostream& print( ostream& ) const; virtual void cuddle();

// ...

деструктор и еще одну виртуальную функцию cuddle():

};

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

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

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

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

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

934

 

 

 

 

деструктор

Panda::~Panda()

 

print(ostream&) const

Panda::print(ostream&)

 

isA() const

Bear::isA()

 

highlight() const

Endangered::highlight()

 

cuddle()

Panda::cuddle()

 

 

 

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

Bear *pb = new Panda;

 

pb->print( cout );

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

pb->isA();

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

pb->cuddle();

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

pb->highlight();

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

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

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()

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

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

935

//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.

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

class Panda : public Bear, public Endangered

раздел 17.6). Например, для нашего объявления класса Panda

{ ... };

Panda yin_yang;

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

Panda ling_ling = yin_yang;

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

Упражнение 18.1

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

(b) class DoublyLinkedList:

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

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

936

(c) class iostream:

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 Дана иерархия классов, обладающая приведенным ниже набором виртуальных функций:

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

937

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();

// ...

};

Base *pb = new MI;

(a) pb->print(); (c) pb->readOn(); (e) pb->log();

Какой экземпляр виртуальной функции вызывается в каждом из следующих случаев:

(b) pb->debug(); (d) pb->writeOn(); (f) delete pb;

Упражнение 18.5

На примере иерархии классов из упражнения 18.4 определите, какие виртуальные функции активны при вызове через pd1 и pd2:

(b) MI obj;

(a)Derived1 *pd1 new MI; Derived2 d2 = obj;