Сложное наследование
Язык C++ допускает не только простое, но и сложное наследование, т. е. наследование от двух и более непосредственных базовых классов. Это позволяет создавать классы, комбинирующие в себе свойства нескольких независимых классов-предков.
Это чаще всего имеет смысл, когда у вас есть некоторый набор понятий, которые могут более или менее независимо комбинироваться с различными элементами другого набора понятий. Простейший пример. Имеется понятие “растение”. Бывают “культурные растения”, и бывают “дикорастущие растения”. С другой стороны, растение может иметь или не иметь “товарной ценности”, т. е. быть полезным или бесполезным с коммерческой точки зрения. Если говорить о товарной ценности, то тут у растений бывают “цветы” и “плоды” и т. д. Все это образует довольно развернутую структуру, которая может порождать понятия вроде “дикое растение, цветы которого можно продавать на рынке”. (Возможно, кстати, и такое: “дикое растение, цветы которого имеют товарную ценность, но которые нельзя продавать на рынке”!) А можно, с некоторыми модификациями, говорить то же самое не о растениях, а о животных или веществах, минералах. И есть не только “товарные” сущности, но и сорняки, вредители. И так далее.
Очевидно, здесь существует ряд довольно независимых категорий — “растение”, “товар”, “культурность” и прочее. Подобная структура — отличный кандидат на реализацию в виде иерархии классов со сложным наследованием.
Кстати, в языке Object Pascal, реализованном в сходном продукте Борланда — Delphi, — нет сложного наследования, что в ряде случаев может значительно ограничить его полезность в сравнении с C++.
Для иллюстрации сложного наследования возьмем последний пример с “сообщениями таймера”. Понятия времени и понятие сообщения — независимые, и, возможно, в программе будут другие классы, родственные “времени” и “сообщению”. Поэтому вполне разумным будет определить для них отдельные классы и породить от них третий класс, применив методику сложного наследования:
#include <stdio.h>
#include <string.h>
//////////////////////////////////////////////////////////////////////
// Базовый класс - время.
//
class Time {
protected;
int hr, min;
public:
Time(int h=12, int m=0): hr(h), min (m):{}
void Show() ;
};
void Time::Show() {
printf("%02d:%02d\n", hr, min);
}
//////////////////////////////////////////////////////////
// Базовый класс - сообщение. //
class Message { protected:
char *msg;
public:
Message(char*) ;
~Message () { delete[]msg; }
void Show () ;
}
Message::Message(char*msg)
// Конструктор Message. {
msg = new char[strlen(str)+1];
strcpy(msg, str);
}
void Message::Show()
{
printf(%s\n", msg);
}
///////////////////////////////////////////////////////
// Производный класс сообщений таймера.
//
class Alarm: public Time, public Message { public:
Alarm(char* str, int h, int m): Time(h, m), Message(str) {}
void Show ();
};
Alarm::Show() // Переопределяет базовые Show().
{
printf("%02d:%02d: %s\n", hr, min, msg);
}
int main() {
Alarm a("Test Alarm!!!", 11, 30);
a.Show() ;
return 0;
}
Вы видите, что конструктор производного класса Alarm имеет пустое тело и список инициализации, вызывающий конструкторы базовых классов. Элементы данных базовых классов объявлены как protected, чтобы можно было непосредственно обращаться к ним в функции Show () производного класса.
Неоднозначности при сложном наследовании
В иерархии классов со сложным наследованием вполне может получиться так, что класс косвенно унаследует несколько экземпляров некоторого базового класса. Если В и С оба являются наследниками A, a D наследует В и С, то D получает двойной набор элементов класса А. Это может приводить к неоднозначностям при обращении к ним, что будет вызывать ошибки времени компиляции. Вот иллюстрация:
class A { public:
int AData;
void AFunc ();
II... };
class B: public A
{
// ... };
class C: public A {
// ...
};
class D: public B, public С // Двукратно наследует А.
{
// ... ,
};
int main (void)
{
D d;
d.AData = 0; // Ошибка! d. AFunc ();
// Ошибка!
return 0;
}
В этом примере строки в main () , содержащие обращения к унаследованным от А элементам, будут вызывать ошибку компиляции с выдачей сообщения о том, что элемент класса неоднозначен. Однако эту неоднозначность несложно устранить, применив операцию разрешения области действия, например, так:
d.B::AData= 0;
d.С::AFunc();
Виртуальные базовые классы
В качестве альтернативы операции разрешения области действия при сложном наследовании, подобном описанному в предыдущем параграфе, можно потребовать, чтобы производный класс содержал только одну копию базового. Этого можно достигнуть, описав базовый класс при наследовании от него как виртуальный с помощью ключевого слова virtual. Вот модификация предыдущего примера, которая делает класс А виртуальным базовым классом:
class A { public:
int AData;
void AFunc ();
// . .. };
class B: public virtual A // A - виртуальный базовый класс.
{
}:
class C: public virtual A // A - виртуальный базовый класс.
{
// ...
};
class D: public B, public С // Содержит только одну копию А.
{
// ...
};
int main(void) {
D d;
d.AData = 0; // Теперь неоднозначности не возникает.
d.AFunc();
//
return 0;
}
Виртуальные базовые классы — более хитрая вещь, чем может показаться с первого взгляда. Если, допустим, конструкторы “промежуточных” классов В и С явно вызывают в своих списках инициализации какой-то конструктор с параметрами класса А, то снова возникает неоднозначность — какой набор параметров компилятор должен использовать при конструировании той единственной копии А, которая содержится в С?
Поэтому, если при конструировании производного класса должен инициализироваться виртуальный базовый класс (пусть даже он инициализируется косвенным образом через конструкторы промежуточных классов), в списке инициализации требуется явно указать инициализатор виртуального базового класса, примерно так:
D: :D(...) : В(. . .) , С(. . .) , А(.. .) {
// ... }
Все выглядит так, как если бы виртуальный базовый класс был непосредственным предком производного класса D “в обход” промежуточных классов иерархии. Поэтому, если требуется вызов конструктора виртуального базового класса, последний обязательно должен присутствовать в списке инициализации производного класса, даже если реально не возникает никакой неоднозначности. Такая ситуация существует, например, в библиотеке OWL, которая имеет иерархию со сложным наследованием и виртуальными базовыми классами. Конструктор главного окна приложения там должен определяться так:
TMyWindow::TMyWindow(TWindow *parent, const char *title):
TFrameWindow(parent, title), TWindow(parent, title) {
// . . . }
TWinoow — виртуальный базовый класс, поэтому он должен инициализироваться отдельно, хотя это выглядит совершенно излишним, так как TFrameWindow, промежуточный класс, сам вызывает конструктор TWindow с теми же параметрами.