Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
OOP.doc
Скачиваний:
6
Добавлен:
25.04.2019
Размер:
1.34 Mб
Скачать

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

При множественном наследовании производный класс может иметь сразу несколько непосредственных базовых классов. Необходимость в таких классах возникает тогда, когда их свойства сочетаются сразу в нескольких других классах. Примером может быть класс CStudentReader, инкапсулирующий свойства и поведение студентов-читателей городской библиотеки. Очевидно, что каждый объект этого класса входит как в число студентов вообще (базовый класс CStudent), так и в число всех читателей библиотеки, некоторые из которых не являются студентами (базовый класс CReader). Еще один пример. Стандартная библиотека С++ включает шаблонный класс basic_iostream, который описывает двунаправленные потоки ввода-вывода. Он является производным сразу от двух шаблонов: basic_istream и basic_ostream. Первый отражает свойства входных потоков, а второй – свойства выходных потоков. Ясно, что basic_iostream – разновидность как basic_istream, так и basic_ostream.

Для того чтобы определить класс, производный от нескольких классов, в формате его определения (в списке наследования) необходимо явно перечислить имена всех базовых классов. При этом к каждому из них может применяться своя схема наследования: закрытое, защищенное или открытое. Один и тот же класс не может появляться в списке наследования более одного раза. Порядок перечисления базовых классов не играет роли.

Ниже для примера дан эскиз определения классов CStudent, CReader и CStudentReader:

class CStudent // первый базовый класс – «студент»

{

public:

CStudent( /* параметры конструктора */ )

{ /* тело конструктора */ }

...

void Study(short term); /* учеба студента, семестр term */

bool PassExam(const string & subject); /* экзамен, предмет subject */

// другие компоненты

...

};

class CReader // второй базовый класс – «читатель»

{

public:

CReader( /* параметры конструктора */ )

{ /* тело конструктора */ }

...

bool OrderBook(const string & title); /* заказ книги title */

// другие компоненты

};

class CReaderStudent: public CStudent, public CReader

// «студент-читатель»

{

public:

CReaderStudent( /* параметры конструктора */ ):

CStudent( /* ... */ ), CReader( /* ... */ )

{ /* тело конструктора */ }

// другие компоненты

...

};

Г рафически иерархию из нашего примера можно представить так, как показано на рисунке 1, где S – класс CStudent, R – класс CReader, а D – производный класс CStudentReader; стрелки показывают отношение «наследует от».

Объект всякого класса, входящего в классовую иерархию, содержит в себе экземпляры каждого базового класса (как прямого, так и непрямого). Например, объект класса CStudentReader будет включать как часть объект класса CStudent и объект класса CReader. Графически это можно представить следующей диаграммой(рис. 2).

Рис. 2

Особенностью множественного наследования в С++ является то, что любой класс может использоваться как косвенный базовый класс многократно; возможна также ситуация, когда класс одновременно является и прямым, и косвенным базовым классом. В этом случае объект производного класса будет включать сразу несколько объектов одного и того же базового класса15. Эта ситуация обусловливает одну из главных проблем множественного наследования – потенциальную неоднозначность. Она заключается в возможном конфликте имен компонент, унаследованных от базовых классов. Например, в приведенных выше классах CStudent и CReader может быть определена функция GetName(), возвращающая имя студента / имя читателя. Эта функция будет унаследована классом CStudentReader фактически дважды: первый раз из класса CStudent, второй раз из класса CReader. В результате ее не получится вызвать для объекта класса CStudentReader без дополнительной квалификации:

CStudentReader * pSR = &StudentReader;

StudentReader –> GetName(); // неоднозначность

StudentReader –> CStudent::GetName(); // нормально

StudentReader –> CReader::GetName(); // тоже нормально

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

К онфликт имен компонент независимых классов происходит не так часто, поэтому описанная проблема, возможно, и не так существенна. Однако, когда один и тот же класс выступает как базовый для другого класса несколько раз, эта проблема возникнет с вероятностью 100%. Классический случай – иерархия, представленная на рисунке 3, где класс A будет базовым для класса D два раза: один раз по цепочке наследования DBA, другой раз – по цепочке DCA.

Е сли использовать обычную схему невиртуального наследования, то каждый объект класса D будет содержать в себе объект класса A дважды и будет дважды наследовать все его компоненты. Фактически получится иерархия, показанная на рисунке 4.

А возможная схема размещения данных объекта класса D в памяти будет подобна приведенной на рисунке 5.

Рис. 5

Если в классе A есть компонент X, то в классе D появятся унаследованные компоненты A::B::D::X и A::C::D::X. При обращении к таким компонентам без их полной квалификации неизбежна ошибка компиляции. Дублирование унаследованных данных, кроме того, вызывает дополнительный расход памяти на объект класса D.

Устранить описанную проблему можно путем введения виртуального наследования. При таком типе наследования базовый класс, от которого возможно многократное косвенное наследование, объявляется виртуальным для всех непосредственных производных классов. Для того чтобы сделать базовый класс виртуальным, к его имени в списке наследования нужно добавить ключевое слово virtual на пример:

class A { /* ... */ };

class B: virtual public A { /* ... */ };

class C: virtual public A { /* ... */ };

class D: public B, public C { /* ... */ };

где классы B и C виртуально наследуют от класса A, за счет чего их наследник – класс D – косвенно наследует от класса A лишь один раз. В результате классовая иерархия приобретает первоначальный вид ромба (см. рис.3), без дублирования базового класса.

Размещение данных объекта класса D при виртуальном наследовании может быть другим (рис.6).

Рис. 6

Данные объекта класса A теперь присутствуют в единственном экземпляре, однако добавляются дополнительные указатели на них от данных объектов классов B и C.

Виртуальное наследование решает проблему потенциальной неоднозначности, однако его применение тоже не лишено трудностей. Первая проблема – вызов конструкторов виртуальных базовых классов. Конструктор любого класса, входящего в иерархию с виртуальным наследованием, должен явно вызывать конструкторы всех виртуальных базовых классов, где бы в иерархии этот класс не находился. Таким образом, разработчик очередного класса иерархии должен знать: какие аргументы требуют конструкторы виртуальных базовых классов. При этом отношение «быть виртуальной базой» передается при наследовании, т.е. все виртуальные базовые классы базового класса автоматически становятся таковыми и для производного класса. Еще одна проблема – доминирование виртуальных функций. Если виртуальный базовый класс содержит виртуальную функцию, которая переопределяется в его непосредственных производных классах, то ниже в иерархии вызов этой функции может быть неоднозначным.

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

Пример

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

{

public:

A(int ia): __ia(ia) {}

virtual void f(); // функция будет переопределяться

...

private:

int __ia;

};

class B: virtual public A

{

public:

B(int ia, int ib): A(ia), __ib(ib) {}

virtual void f(); // переопределение

...

private:

int __ib;

};

class C: virtual public A

{

public:

C(int ia, int ic): A(ia), __ic(ic) {}

// virtual void f(); // переопределение

...

private:

int __ic;

};

class D: public B, public C

{

public:

D(int ia, int ib, int ic): // необходим параметр ia для класса A

A(ia), B(ia,ib), C(ia,ic)

// необходим вызов конструктора класса A

// в B(ia,ib) и C(ia,ic) аргумент ia фиктивный

{}

...

};

class E: public D // новый класс разработчика

{

public:

E(int ia, int ib, int ic): // необходим параметр ia для класса A

A(ia), D(ia,ib,ic) // необходим вызов конструктора класса A

// в вызове D(ia,ib,ic) аргумент ia фиктивный

{}

...

};

Как видно из определений классов D и E, их конструкторы явно вызывают конструктор виртуального базового класса A, а также конструкторы непосредственных базовых классов. Причем порядок записи вызовов конструкторов не играет роли – все равно первыми будут вызваны конструкторы виртуальных базовых классов. В этом состоит проявление первой проблемы. Вторая проблема иллюстрируется функцией f. Она введена в классе A и переопределяется в классах B и C (в C заголовок функции взят в комментарий). В таком виде иерархия будет успешно скомпилирована. Но если убрать комментарий с функции C::f, наследование этой функции в классах D, E становится неоднозначным. Это и есть проявление проблемы доминирования – неясно какой вариант переопределения виртуальной функции использовать далее в иерархии B::f или C::f.

С++ позволяет сочетать виртуальное наследование с невиртуальным. Примером является иерархия классов, изображенная на рисунке 7. В ней класс A является обычным базовым классом, класс Q – виртуальным базовым классом, а класс V – одновременно и обычным, и виртуальным (смешанным) базовым классом. Объект класса D будет включать 2 копии объекта класса A, одну копию объекта класса Q и по одной копии объекта виртуального и невиртуального класса V. Ниже дано возможное определение классов данной иерархии.

Рис. 7

class A

{

public:

A(int ia): __ia(ia) {}

private:

int __ia;

};

class Q

{

public:

Q(int iq): __iq(iq) {}

private:

int __iq;

};

class V

{

public:

V(): __iv(0) {}

V(int iv): __iv(iv) {}

private:

int __iv;

};

class B: virtual public Q, virtual public V, public A

{

public:

B(int iq, int iv, int ia, int ib): Q(iq), V(iv), A(ia), __ib(ib) {}

private:

int __ib;

};

class C: virtual public Q, virtual public V, public A

{

public:

C(int iq, int iv, int ia, int ic): Q(iq), V(iv), A(ia), __ic(ic) {}

private:

int __ic;

};

class D: public B, public C, public V

{

public:

D(int ia, int ib, int ic, int iq, int iv):

V(iv), Q(iq), B(iq,iv,ia,ib), C(iq,iv,ia,ic)

// вызов V(iv) инициализирует объект виртуальной базы V

{}

};

Интересная особенность характерна для конструктора класса D. Так как для класса D класс V является базовым дважды, необходимо дважды вызывать конструктор V::V(int). Но это запрещено синтаксисом С++. Выход из этой ситуации таков. Конструктор V::V(int) инициализирует объект виртуального базового класса V, а объект невиртуального базового класса V инициализируется конструктором по умолчанию V::V().

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]