C++ Программирование в среде С++ Builder 5

         

Сложное наследование


Язык 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 с теми же параметрами.


Содержание раздела