Архитектура Аудит Военная наука Иностранные языки Медицина Металлургия Метрология Образование Политология Производство Психология Стандартизация Технологии |
Множественное наследование
Для поддержки множественного наследования синтаксис списка базовых классов 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(), ~Endangered(), ~Bear(), ~ZooAnimal(). В разделе 17.3 уже говорилось, что в случае одиночного наследования к открытым и защищенным членам базового класса можно обращаться напрямую (не квалифицируя имя члена именем его класса), как если бы они были членами производного класса. То же самое справедливо и для множественного наследования. Однако при этом можно унаследовать одноименные члены из двух или более базовых классов. В таком случае прямое обращение оказывается неоднозначным и приводит к ошибке компиляции. Однако такую ошибку вызывает не потенциальная неоднозначность неквалифицированного доступа к одному из двух одноименных членов, а лишь попытка фактического обращения к нему (см. раздел 17.4). Например, если в обоих классах Bear и Endangered определена функция-член print(), то инструкция ying_yang.print( cout ); приводит к ошибке компиляции, даже если у двух унаследованных функций-членов разные списки параметров.
Endangered:: print( ostream&, int )
Endangered:: print( ostream&, int ) Причина в том, что унаследованные функции-члены не образуют множество перегруженных функций внутри производного класса (см. раздел 17.3). Поэтому print() разрешается только по имени, а не по типам фактических аргументов. (О том, как производится разрешение, мы поговорим в разделе 18.4.) В случае одиночного наследования указатель, ссылка или объект производного класса при необходимости автоматически преобразуются в указатель, ссылку или объект базового класса, которому открыто наследует производный. Это остается верным и для множественного наследования. Так, указатель, ссылку или сам объект класса Panda можно преобразовать в указатель, ссылку или объект ZooAnimal, Bear или Endangered:
cout < < ying_yang < < endl; // правильно Однако вероятность неоднозначных преобразований при множественном наследовании намного выше. Рассмотрим, к примеру, две функции:
extern void display( const Endangered& ); Неквалифицированный вызов 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& );
Компилятор не может различить два непосредственных базовых класса с точки зрения преобразования производного. Равным образом применимы обе трансформации. (Мы покажем способ разрешения этого конфликта в разделе 18.4.) Чтобы понять, какое влияние оказывает множественное наследование на механизм виртуальных функций, определим их набор в каждом из непосредственных базовых классов Panda. (Виртуальные функции введены в разделе 17.2 и подробно обсуждались в разделе 17.5.)
}; Теперь определим в классе Panda собственный экземпляр print(), собственный деструктор и еще одну виртуальную функцию cuddle():
}; Множество виртуальных функций, которые можно напрямую вызывать для объекта Panda, представлено в табл. 18.1. Таблица 18.1. Виртуальные функции для класса Panda
Когда ссылка или указатель на объект Bear или ZooAnimal инициализируется адресом объекта Panda или ему присваивается такой адрес, то части интерфейса, связанные с классами Panda и Endangered, становятся недоступны:
delete pb; // правильно: Panda:: ~Panda() (Обратите внимание, что если бы объекту класса Panda был присвоен указатель на ZooAnimal, то все показанные выше вызовы разрешались бы так же.) Аналогично, если ссылка или указатель на объект Endangered инициализируется адресом объекта Panda или ему присваивается такой адрес, то части интерфейса, связанные с классами Panda и Bear, становятся недоступными:
delete pe; // правильно: Panda:: ~Panda() Обработка виртуального деструктора выполняется правильно независимо от типа указателя, через который мы уничтожаем объект. Например, во всех четырех инструкциях порядок вызова деструкторов один и тот же – обратный порядку вызова конструкторов:
delete pe; Деструктор класса Panda вызывается с помощью механизма виртуализации. После его выполнения по очереди статически вызываются деструкторы Endangered и Bear, а в самом конце – ZooAnimal. Почленная инициализация и присваивание объекту производного класса, наследующего нескольким базовым, ведут себя точно так же, как и при одиночном наследовании (см. раздел 17.6). Например, для нашего объявления класса Panda
{... }; в результате почленной инициализации объекта ling_ling
Panda ling_ling = yin_yang; вызывается копирующий конструктор класса Bear (но, так как Bear производный от ZooAnimal, сначала выполняется копирующий конструктор класса ZooAnimal), затем – класса Endangered и только потом – класса Panda. Почленное присваивание ведет себя аналогично. Упражнение 18.1 Какие из следующих объявлений ошибочны? Почему? (a) class CADVehicle: public CAD, Vehicle {... };
public List, public List {... };
private istream, private ostream {... }; Упражнение 18.2 Дана иерархия, в каждом классе которой определен конструктор по умолчанию:
class MI: public C, public Z {... }; Каков порядок вызова конструкторов в таком определении: MI mi; Упражнение 18.3 Дана иерархия, в каждом классе которой определен конструктор по умолчанию:
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 Дана иерархия классов, обладающая приведенным ниже набором виртуальных функций:
}; Какой экземпляр виртуальной функции вызывается в каждом из следующих случаев:
(b) pb-> debug(); (d) pb-> writeOn(); (f) delete pb; Упражнение 18.5 На примере иерархии классов из упражнения 18.4 определите, какие виртуальные функции активны при вызове через pd1 и pd2: (a) Derived1 *pd1 new MI;
Derived2 d2 = obj; |
Последнее изменение этой страницы: 2019-04-09; Просмотров: 320; Нарушение авторского права страницы