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

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

Рис. 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& )

Endangered::print( ostream&, int )

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

Bear::print( ostream& )

Endangered::print( ostream&, int )

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

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

extern void display( const Bear& );

extern void highlight( const Endangered& );

Panda ying_yang;

display( ying_yang ); // i?aaeeuii

highlight( ying_yang ); // i?aaeeuii

extern ostream&

operator( ostream&, const ZooAnimal& );

cout ying_yang endl; // правильно

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

extern void display( const Bear& );

extern void display( const Endangered& );

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

Panda ying_yang;

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 и подробно обсуждались в разделе 17.5.)

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;

// ...

};

Теперь определим в классе 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 ); // i?aaeeuii: Panda::print(ostream&)

pb-isA(); // i?aaeeuii: Bear::isA()

pb-cuddle(); // ioeaea: yoi ia ?anou eioa?oaena Bear

pb-highlight(); // ioeaea: yoi ia ?anou eioa?oaena Bear

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

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

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

Endangered *pe = new Panda;

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

// ioeaea: yoi ia ?anou eioa?oaena Endangered

pe-cuddle();

pe-highlight(); // правильно: Endangered::highlight()

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

{ ... };

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

Panda yin_yang;

Panda ling_ling = yin_yang;

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

Упражнение 18.1

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

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

(b) class DoublyLinkedList:

public List, public List { ... };

(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

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

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:

(a) Derived1 *pd1 new MI;

(b) MI obj;

Derived2 d2 = obj;

Поделитесь на страничке

Следующая глава >

Похожие главы из других книг

2. Наследование

Из книги Информатика и информационные технологии: конспект лекций автора Цветкова А В

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


26. Наследование

Из книги Информатика и информационные технологии автора Цветкова А В

26. Наследование Наследование – это процесс порождения новых типов-потомков от существующих типов-родителей, при этом потомок получает (наследует) от родителя все его поля и методы.Тип-потомок, при этом, называется наследником или порожденным (дочерним) типом. А тип,


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

Из книги Язык программирования С# 2005 и платформа .NET 2.0. [3-е издание] автора Троелсен Эндрю

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


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

Из книги Microsoft Visual C++ и MFC. Программирование для Windows 95 и Windows NT автора Фролов Александр Вячеславович

Наследование Пожалуй, самая важная возможность, предоставляемая программисту средствами языка Си++, заключается в механизме наследования. Вы можете наследовать от определенных ранее классов новые производные классы. Класс, от которого происходит наследование,


Единичное наследование

Из книги Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ автора Мейерс Скотт

Единичное наследование В случае единичного наследования порожденный класс наследуется только от одного базового класса. Рисунок 1.1 отражает единичное наследование классов. Единичное наследование является наиболее распространенным методом наследования. Библиотека


Правило 34: Различайте наследование интерфейса и наследование реализации

Из книги Основы объектно-ориентированного программирования автора Мейер Бертран

Правило 34: Различайте наследование интерфейса и наследование реализации Внешне простая идея открытого наследования при ближайшем рассмотрении оказывается состоящей из двух различных частей: наследования интерфейса функций и наследования их реализации. Различие


Множественное наследование (Multiple inheritance)

Из книги TCP/IP Архитектура, протоколы, реализация (включая IP версии 6 и IP Security) автора Фейт Сидни М

Множественное наследование (Multiple inheritance) Часто необходимо сочетать различные абстракции. Рассмотрим класс, моделирующий понятие "младенец". Его можно рассматривать как класс "человек" с компонентами, связанными с этим классом. Его же можно рассматривать и более прозаично


Наследование и децентрализация

Из книги Программирование на языке Ruby [Идеология языка, теория и практика применения] автора Фултон Хэл

Наследование и децентрализация Имея динамическое связывание, можно создавать децентрализованные архитектуры ПО, необходимые для достижения целей повторного использования и расширяемости. Сравним ОО-подход, при котором самодостаточные классы предоставляют свои


У14.4 Наследование без классов

Из книги C++ для начинающих автора Липпман Стенли

У14.4 Наследование без классов В этой лекции были представлены два взгляда на наследование: будучи модулем, класс-наследник предлагает службы своего родителя плюс еще некоторые, будучи типом, он реализует отношение "является" (каждый экземпляр наследника является также


Лекция 15. Множественное наследование

Из книги Linux Mint и его Cinnamon. Очерки применителя автора Федорчук Алексей Викторович

Лекция 15. Множественное наследование Полноценное применение наследования требует важного расширения этого механизма. Изучая его основы, мы столкнулись с необходимостью порождать новые классы от нескольких классов-родителей. Эта возможность, известная как


5.21 IP-адреса, интерфейсы и множественное пребывание

Из книги Описание языка PascalABC.NET автора Коллектив РуБоард

5.21 IP-адреса, интерфейсы и множественное пребывание Идентификация сетей и подсетей в IP-адресе имеет много достоинств:? Упрощается работа по присваиванию адресов. Блок адресов можно делегировать для администрирования в отдельной сети или подсети.? Сокращаются таблицы


1.1.2. Наследование

Из книги автора

1.1.2. Наследование Мы подходим к одной из самых сильных сторон ООП — наследованию. Наследование —- это механизм, позволяющий расширять ранее определенную сущность путем добавления новых возможностей. Короче говоря, наследование - это способ повторного использования


18. Множественное и виртуальное наследование

Из книги автора

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


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

Из книги автора

18.2. Множественное наследование Для поддержки множественного наследования синтаксис списка базовых классовclass Bear : public ZooAnimal { ... };расширяется: допускается наличие нескольких базовых классов, разделенных запятыми:class Panda : public Bear, public Endangered { ... };Для каждого из перечисленных


Перенаправление расширенное и множественное

Из книги автора

Перенаправление расширенное и множественное Что такое перенаправление ввода/вывода — знают все применители CLI. Однако в Zsh возможности его очень широки, почему оно и называется здесь расширенным перенаправлением. Этот механизм позволяет в ряде случаев обходиться без


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

Из книги автора

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