Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
лек_3_слайды.doc
Скачиваний:
1
Добавлен:
17.11.2019
Размер:
457.73 Кб
Скачать

Virtual void f1(void);

// Обычная виртуальная функция-член

virtual void f2(void)=0;

// f2() Чистая виртуальная функция-член

..

}

Компилятору не потребуется реализация функции-члена f2(), в отличие от остальных функций-членов, объявленных в классе.

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

Абстрактный класс может использоваться только в качестве базового для других классов — объ­екты абстрактного класса создавать нельзя, поскольку прямой или косвенный вы­зов чисто виртуального метода приводит к ошибке при выполнении

В С++ нельзя создать объекты, имеющие типы абстрактных классов.

Например, не сможем объявить объект типа AbstractClass.

AbstractClass myObject; // ?????

компилятор выдаст сообщение об ошибке “cannot create an instance of class’ AbstractClass’” (не могу создать объект класса ‘AbstractClass’).

Другие функции-члены класса AbstractClass могут вызывать чистую виртуальную функцию-член. Например, функция-член f1() может вызвать f2():

Void AbstractClass::f1(void)

{

f2();

// Вызвать чистую виртуальную функцию-член

. // Прочие операторы f1()

}

Производный класс MyClass наследует чистую виртуальную функцию-член, но объявляет ее без = 0. (Включение этого фрагмента сделает класс MyClass абстрактным.) где-нибудь в другом месте следует реализовать функцию-член f2():

Void MyClass::f2(void)

{

. // Оператор функции-члена

}

Обращения к первоначальной чистой виртуальной функции-члену, как это реализовано, к примеру, в данном случае с AnyClass::f1(), теперь переадресованы MyClass::f2().

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

Чистые виртуальные функции подобны, на которые можно цеплять код производных классов.

Теперь, когда все виртуальные функции-члены реализованы, компилятор сможет воспринять объекты типа MyClass:

MyClass myObject;

Выполнение оператора

myObject.f1 ();

приведет к вызову наследованной f1(), которая вызовет f2() из MyClass.

практический пример использования чистых виртуальных функций и абстрактных классов.

Класс TSet может сохранять простое множество объектов произвольного типа. В листинге 14ю5 содержится объявление класса, а также другие вспомогательные объявления.

ПРИМЕР 3.5 SET.H (объявление класса TSet)

1: // set. h – объявление класса TSet

2:

3: # ifndef _ _SET_H

4: # define _ _SET_H 1

// Предотвращение нескольких # include

5://Неполные объявления класса

6: Class tElem ;

7: typedef TElem* PTElem;

8: typedef PTElem* PPTElem;

9:

10: class TSet {

11: typedef TSet* PTSet

12:

13: class TSet {

14: private:

15: int max;

// Максимальное число элементов

16: int index;

// Индекс массива множества

17: PPTElem set;

// Указатель на массив указателей на PTElem

18: protected:

19 virtual int CompareElems(PTElem p1, PTElem p2) = 0;

20: public:

21: TSet(int n)

22: {max = n; index = 0; set = new PTElem [n];}

23: virtual TSet()

24: {delete[] set; }

25: void AddElem(PTElem p);

26: int HasElem(PTElem p);

27: };

28:

29: #endif // __SET-H

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

Как показано в строке 6, С++ позволяет делать неполные объявления классов, не имеющих тела:

class TElem ; // неполные объявления класса

В соответствии с этими объявлением объекты классы TElem могут задаваться в параметрах функций, возвращается функциями и т.д. Конечно,

позже в программе должно присутствовать полное объявление класс TElem. Неполное описание класса дает возможность написания класса TSet без каких-либо привязок к конкретному типу объектов, которые будут запоминаться в классе.

В строках объявляются два дополнительных символа, облегчающих чтение листинга. TElem объявлен алиасом для указателя TElem, т.е. указателем на указатель на объект типа TElem. В классе TSet алиас PPTElem используется для создания динамически изменяющего свой размер массива указателей PTElem.

В строках 10-11 похоже объявления используется для задания неполного описания класса Test и алиаса PTSet в качестве указателя на объекты TSet. Единственная причина неполного объявления Test в строке 10 – возможность компиляции последующего typedef-объявления.

Затем следует объявление класса TSet (строки 13-27). Три закрытых данных-членов скрывают детали внутренней реализации класса. В этой версии TSet множество храниться в массиве указателей TElem, динамически изменяющем свой размер, указатель PPTElem используется для хранения адреса массива. (член set – указатель на массив указателей на объекты типа TElem.) члены max и index используются для управления массивом.

Совет

Поскольку все данные-члены класса TSet закрыты в класс, их можно изменить позже, не затрагивая операторы, использующие объекты класса. При написание классов часто бывает полезно выбирать простые, если не идеально эффективные, методы сохранения данных класса. Как только вы сделаете эти данные закрытыми в классе, вы всегда сможете изменить их позже, не затрагивая код, не принадлежащий классу.

В строке 19 объявляется чистая виртуальная функция-член CompareElems как защищенный член класса Test. Чистые виртуальные функции-члены также могут быть и открытыми, но чаще всего они защищены, поскольку предполагается, что функция будет реализовано в производном классе. Предполагается, что функция-член ComparElems() возвращает нуль, если два объекта класса TElem, на которые ссылаются указатели p1 и p2, идентичны. Очевидно, вы все еще не можете завершить функцию ComparElems(), поскольку вы не имеете ни малейшего представления о том, что содержит класс TElem. Конечно, с помощью виртуальных функций-членов вы можете, по крайне мере, завершить обобщенную планировку, необходимую для класса TSet.

Это планировка объявлена и частично реализовано в строках 22-26. Встраиваемый конструктор класса (строки 21-22) инициализирует закрытые данные-члены класса и обращается к new для выделения массива указателей PTElem. Деструктор класса (строки 23-24) освобождает эту память, очищая все объекты TSet перед их выходом из области видимости.

Функции-члены AddElem() и HasElem() (строки 25-26) слишком длинны для того, чтобы сделать их встраиваемыми. Модуль реализации класса (листинг 3.6 ) дополняет эти функции, (вы можете скомпилировать эту программу, но для того, чтобы использовать класс, необходимо основная программа. Пример приводится в следующем разделе.)

Листинг 3.6. SET.CPP (реализация класса TSet)

1: // set.cpp -- Реализация класса TSet

2:

3: #include <iostrem.h>

4: #include <stdio.h>

5: #include ”set.h”

6:

7://Добавляет элемент в множество.

(в случае ошибки программа прерывается!)

8: void TSet::AddElem(PTElem p)

9: {

10: if (set = = NULL) {

11: cout << “\nERROR; Out of memory”;

12: exit(1);

13: }

14: if (index >= max) {

15: cout << “\nERROR: Set limit exceeded”;

16: exit(1);

17: }

18: set[index] = p;

19: ++index;

20: }

21:

22: // Возвращает истину, если элемент,

адресуемый p, находятся в множестве

23: int TSet::HasElem(PTElem p)

24: {

25: if (set = = null)

26: return 0;

// В пустом множестве не может быть элементов

27: for (int i = 0; I < index; i++)

28: if (CompareElems(p, set[i]) = = 0)

29: return 1; // Элемент в множестве

30: return 0; // элемент в множестве нет

31: }

В SET.CPP реализуется две функции – AddElem() и HasElem(). Любопытно, что эти функции написаны до того , как будут разработаны элементы, которыми они оперируют. Все, что известно об элементах это лишь то, что они имеют тип неопределенного класса с именем TElem.

Но даже в этом случае AddElem() (строки 8-20) добавляет в множестве объект класса TElem, адресуемые параметром p. В строках 10-17 осуществляется проверка состояния ошибки и аварийного завершения программы в случае нехватки памяти или переполнения множества. (в более сложных классах обеспечивается лучшая обработка ошибок, однако для данного примера вполне достаточно и такой минимальный поддержки.)

В строках 18-19 объект TElem, на который ссылается указатель p, запоминается в массиве set.

Функция-член HasElem() (строки23-31) возвращает истину , если переданный объект TElem находится в множестве. Функция HasElem() вызывает чистую виртуальную функцию-член класса CompareElemas() в строке 28. несмотря на то что эта функция еще даже не существует, ее уже можно вызвать! В производном классе со временем будет реализован настоящий код, осуществляющий сравнение двух элементов. Благодаря виртуальным функциям-членам, оператор в строке 28 может обратиться к еще не реализованному коду.

В программе TEST.CPP (листинг 3.7) поставляются недостающие части TSet и TElem для создания множества строк, содержащих названия месяцев, имеющих ровно 30 дней: апрель, июнь, сентябрь, и ноябрь.

Программа иллюстрирует, как виртуальные функции-члены и абстрактные классы могут обеспечить простоту разработки программ, которые будут завершены позднее. Для создания законченной программы следует скомпилировать и скомпоновать модули TEST.CPP и SET.CPP. находясь в DOS, введите команду bcc test set , затем запустите на выполнение получившийся в результате исполняемой файл TEST.EXE , введя его имя. или же из интегрированной среды откройте файл проект TSET.IDE с помощью меню project и нажмите <ctrl+F9> для компиляции и выполнения программы в виде EasyWin-приложения.

Листинг 3.7 TSET.CPP

(тестирование класса TSet)

1: #include <iostrem.h>

2: #include <stdio.h>

3: #include ”set.h”

4:

5: class TElem {

6: private:

7: char *sp;

// указатель на элемент-строку

8: public:

9: TElem(const char *s) { sp = strdup(s);}

10: virtual TElem() { delete sp; }

11: virtual const char *GetString(void) {return sp;}

12: };

13:

14: class TMySet: public TSet {

15: protected:

16: virtual int CompareElems(PTElem p1, PTElem p2);

17: public:

18: TMySet(int n): TSet(n) {}

19: {;

20:

21: void test(const char *s, PTSet setp);

22:

23: main()

24: {

25: TMyset thirties(12);

// множество из 12 объектов TElem

26:

27: thirties.AddElem(new TElem(“Sep”));

28: thirties.AddElem(new TElem(“Apr”));

29: thirties.AddElem(new TElem(“Jun”));

30: thirties.AddElem(new TElem(“Nov”));

31: Test(“Jan”, &thirties);

32: Test(“Fe”, &thirties);

33: Test(“Mar”, &thirties);

34: Test(“Apr”, &thirties);

35: Test(“May”, &thirties);

36: Test(“Jun”, &thirties);

37: Test(“Jul”, &thirties);

38: Test(“Aug”, &thirties);

39: Test(“Sep”, &thirties);

40: Test(“Oct”, &thirties);

41: Test(“Nov”, &thirties);

42: Test(“Des”, &thirties);

43: return 0;

44: }

45:

46: // Сообщает, принадлежит ли строка s множеству step

47: void test(const char *s, PTSet setp)

48: {

49: TElem testElem(s);

50:

51: if (setp->HasElem(&testElem))

52: cout << s << “ is not in the set\n”;

53: else

54: cout << s << “ is not in the set\n”;

55: }

56:

57: // Возвращает нуль, если два элемента идентичны, иначе – не нуль

58: int TMySet:: CompareElems(PTElem p1, PTElem p2);

59: {

60: return (stricmp(p1->GetString(), p2->GetString()));

61: }

Первый шаг к использованию класса TSet – написание класса TElem. Объекты класса TElem сохраняются в множестве. В строках 5-12 объявляется образец класса TElem, способный запомнить строковые значения. Все функции-члены класса реализованы встраиваемыми.

В строках 14-19 из абстрактного класса TSet выводится новый класс TMySet . конструктор нового класса не выполняет никаких дополнительных действий, он просто вызывает конструктор базового класса TSet(). Более существенно то, что в строке 16 объявляется виртуальная функция-член CompareElems(), объявленная чистой виртуальной функцией в классе TSet.

Пропустим все до конца листинга, где приводится реализация функции-члена CompareElems(). Эта функция вызывает библиотечную функцию stricmp() для сравнения двух строк. Доступ к этим строкам осуществляется путем обращение к функции-члену GetString() класса TElem (см. также строку 11). При вызове строке 51 HasElem() эта функция-член вызывает реализованную в строках 58-61 CompareElems(). Вы можете запустите получившуюся программу в Turbo Debugger и проанализировать вызов этой функции для наблюдения за тем, как программа переадресует CompareElems().

Данный пример использования виртуальных функций-членов и абстрактных классов приводит к нескольким интересным наблюдениям.

  • Модуль SET можно скомпилировать заранее и сохранить в библиотеке классов. Пользователям не нужен файл SET.CPP с исходным текстом класса TSet. Основной программе необходим лишь заголовочный файл SET.H.

  • Другая программа может использовать класс TSet для хранения множества различных классов объекта TElem. Не нужно повторно компилировать SET.CPP для каждого нового использования класса. Класс TSet обеспечивает лишь не которые рудиментарные операции над множеством. Пользователю класса предоставляется реализация прочих деталей.

  • И наконец, при проектировании абстрактных классов, подобных TSet, программисты обычно помещают в класс множество виртуальных функций-членов на тот случай, если когда-нибудь понадобятся. При создании абстрактных классов постарайтесь обойтись минимумом необходимых членов, вместо того чтобы пытаться охватить возможно большую область применения. Если ваши классы будут слишком сложны, другие программисты (и даже впоследствии вы сами) могут быть обескуражены перспективой вывода новых классов из ваших.

Виртуальные деструкторы

Функции-члены и деструкторы, но не конструкторы, могут быт виртуальными.

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

Например, рассмотрим следующий класс, который может запоминать строковое значение:

class TBase {

private:

char *sp1;

public:

TBase(const char *s)

{

//выделяет пространство для строки

// сохраняет адрес новой строки в указателе

sp1 = strdup(s); }

virtual ??TBase()

{ delete sp1; }

// виртуальный деструктор //освобождает эту память, когда //объект типа TBase выходит из //области видимости.

};

Конструктор класса TBase выделяет пространство для строки путем обращение к функции strdup() и сохраняют адрес новой строки в указателе sp1. виртуальный деструктор освобождает эту память, когда объект типа TBase выходит из области видимости.

Введем новый класс из TBase:

class TDerived: public TBase {

private:

char *sp2;

public:

TDerived(const char *s1, const char *s2): TBase(s1)

{ sp2 = strdup(s2); }

virtual TDerived()

{delete sp2;}

};

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

Когда объект TDerived выходит из области действия, важно, чтобы обе копии строки были удалены. Предположим, вы объявили указатель на класс TBase, но присвоили ему адрес объекта TDerived – это вполне допустимо, поскольку, как вам уже известно, указатель на базовый класс может ссылаться на объект этого класса или на объект любого производного класса. Программа на этой стадии выглядит следующим образом:

TBase *pbase;

pbase = new TDerived(“String 1”, “string 2”);

Рассмотрим что произойдет, когда программа удалит объект, на который ссылается pbase:

delete pbase; // !!!

Компилятору было указано, что pbase ссылается на объекты типа TBase и программа вызвала бы деструктор TBase при удалении объекта, на который ссылается pbase. Но, указатель pbase в действительности ссылается на объект класса TDerived, и деструктор этого класса должен быть вызван так же, как и деструктор базового класса.

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

Итоги:

При определении абстрактного класса необходимо иметь в виду следующее:

  • абстрактный класс нельзя использовать при явном приведении типов, для описания типа параметра и типа возвращаемого функцией значения;

  • допускается объявлять указатели и ссылки на абстрактный класс, если при инициализации не требуется создавать временный объект;

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

Таким образом, можно создать функцию, параметром которой является указа­тель на абстрактный класс. На место этого параметра при выполнении програм­мы может передаваться указатель на объект любого производного класса.

Это по­зволяет создавать полиморфные функции, работающие с объектом любого типа в пределах одной иерархии.

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

До сих пор в этой главе производные и базовые классы были связаны отношением простого наследования.

Множественное наследование описывает более, чем одного базового класса. На рис. 3.12 иллюстрируется концепция

Новые данные-члены

Базовый класс А

Базовый класс В

Базовый класс С

Производный класс

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

Подключение базовых классов

Для вывода нового класса из нескольких базовых следует перечислить имена базовых классов после имени нового. Если А,В и С – классы, то новый класс D может наследовать их все следующим образом:

class D: public A, public B, public C {

……

};

Можно объявить несколько базовых классов закрытыми или защищенными в производном классе. Или же можно использовать спецификаторы доступа вперемежку:

class D: public A, public B, C {

….

};

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

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

В случае конфликта имен (например, в двух базовых классах объявляется функция-член с одним и тем же именем) следует использовать оператор разрешения видимости для задания нужного вам члена класса.

Например, предположим, что классы А и В оба имеют открытую функцию-член Display(). В производном классе

class D: public A, public B {

public:

void f(void);

……

};

в функции-члене f() необходимо указать, какую из конфликтующих функций

Display() следует вызвать. Нужного эффекта можно добиться, задав имя соответствующего класса и оператора разрешения области видимости перед вызовом функции:

void D::f(void)

{

Display();

//??? Двусмысленно; не скомпилируется

A::Display();

// Вызов функции-члена А

B::Display();

//Вызов функции-члена В

}

использование конструкторов нескольких базовых классов

В классе, выведенном из нескольких базовых, также может возникнуть необходимость в вызове конструкторов этих классов. Если классы А, В и С имеют конструкторы по умолчанию, в производном классе D их можно вызвать следующим образом:

class D: public A, public B, public C {

public:

D(): A(), B(), C() {}

// Встраиваемый конструктор

….

};

Или же конструктор можно объявить как

class D: public A, public B, public C {

public:

D();

// конструктор, реализованный снаружи

……

};

и реализовать отдельно, обычно, в другом файле:

D:D()

: A(), B(), C()

{

….. // операторы в конструкторе D()

}

Конечно, конструкторы могут иметь различные параметры, не использующиеся в данном примере. Конструкторы вызываются в том порядке , в котором объявлены из базовые классы. Было бы неразумно придумывать классы, зависящие от ого, в коком порядке будут созданы объекты других классов. Как правило, классы должны действовать независимо друг от друга настолько, насколько это вообще возможно.

Замечание

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

Использование виртуальных базовых классов

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

Легко понять, почему возникают конфликты в сложной иерархии классов, особенно когда дело касается множественного наследования. Один из самых распространенных конфликтов возникает, когда производный класс наследует слишком много копий некоторых базовых классов, подобно победителю лотереи, который в один прекрасный день обнаруживает, что у него невероятное количество “троюродных сестер”, о существовании которых он раньше даже не подозревал.

Для демонстрации того, как в производных классах возникают неприятности, связанные с наследованием, ознакомьтесь с классами программы, приведенной в листинге 3.8 .В программе используется сложная взаимосвязь между тремя фиктивными компаниями и трем вымышленными инвесторами: бобом (Bob), Тедом (Ted) и Алисой (Alice). Программа может показаться неудачной шуткой, однако заимствование тесных взаимосвязей из реальной жизни поможет объяснить типичные и зачастую доводящие до белого каления проблемы, возникающие с множественным наследованием.

Листинг 3.8. FRANCH.CPP (множественное наследование и вкладчики)

1: // franch.cpp

2:

3: #include <iosream.h>

4: #include <string.h>

5:

6: class company {

7: private:

8: char *name

9: public:

10: Company(const char *s)

11: name = strdup(s);

12: cout << “In constructor for”;

13: Display();

14: }

15: virtual ??company() {

16: cout << “In destructor for “;

17: Display();

18: delete name;

19: }

20: void Display(void) {cout << name << ‘\n’; }

21: };

22:

23: class Jennys; public Company {

24: public:

25: Jennys(); Company(”Jenny’s”) {}

26: };

27:

28: class McDougles: public Company {

29: public:

30: McDougles(): Company(“McDougles”) {}

31: };

32:

33: class BurgerQueen: public Company {

34: public:

35: BurgerQueen(): Company(“BurgerQueen”) {}

36: };

37:

38: class Bob

39: : public Jennys,

40: public McDougles {

41: };

42:

43: class Bob

44: : public McDougles,

45: public BurgerQueen {

46: };

47:

48: class Alice

49: : public Jennys,

50: public McDougles,

51: public BurgerQueen {

52: };

53:

54: main()

55: {

56: Bob *bobp;

57: Ted *tedp;

58: Alice *alicep;

59:

60: cout << “\nInitializing Bob’s resturant\n”;

61: bobp = new Bob;

62: cout <<” Initializing Ted’s resturant\n”;

63: tedp = new Ted;

64: cout <<” Initializing Alice’s resturant\n”;

65: alicep = new Alice;

66:

67: cout <<”\nDeleting Bob’s resturant\n”;

68: delete bobp;

69: cout <<”Deleting Ted’s resturant\n”;

70: delete tedp;

71: cout <<”Deleting Alice’s resturant\n”;

72: delete alicep;

73:

74: return 0;

7 5: }

Jennys

Bob

Company

McDougles

Ted

BurgerQueen

Alice

Рис. 3.13. Иерархия классов в программе FRANCH.CPP

На рис. 3.13 иллюстрируется родословная классов в FRANCH.CPP. в корне иерархии – класс Company, служащий базовым для трех производных классов – Jennys, McDouglas и BurgerQueen. Каждая из этих “компаний ”-классов выводится из класса Company, и, следовательно, каждый класс наследует член name (строка 8), а также функцию-член Display() (строка 20).

Три предприимчивых инвестора, Боб, Тед и Алиса, объявлены классами (строки 38-52). Класс Bob строит свою кулинарную империю из двух классов Company – Jennys и McDougles. Класс Ted делает себе состояние на McDougles и BurgerQueen.Alice, самая честолюбивая душа из троицы, вкладывает средства сразу в три класса Company – Jennys, McDougles и BurgerQueen. Когда вы запустите программу то, увидите сообщения, рапортующие, какие конструкторы и деструкторы и когда был запущены, как они инициализировали и очищали три объекта классов, по одному на каждого вкладчика (строки 56-58).

Совет

При отладке вашего собственного кода вы можете использовать операторы отображения, подобные тем, что использовались в FRANCH.CPP. отображение сообщений из конструкторов, деструкторов или функций-членов – полезная обратная связь с сложной иерархической организацией классов.

Иерархия классов в FRANCH.CPP на первый взгляд прост. Однако настоящие проблемы с родством классов возникают, когда еще один класс выводится из группы компаний и инвесторов. Предположим, еще одна корпорация приобретает некоторые из компаний-родителей и избранных вкладчиков. Как и в реальном мире корпоративных финансов, сложные взаимосвязи между компаниями и людьми могут легко стать неуправляемыми. Рассмотрим класс Corporation, в котором делается попытка наследования компании McDougles вместе с акционерами Тедом и Алисой:

class Corporation

: public McDougles

public Ted,

public Alice {

public:

….

};

Вернемся назад, к рис. 3.13, и вы удалите, почему версия Corporation никогда не оторвется от земли. (этот код даже не скомпилируется.) Ted и Alice уже частично выведены из McDougles. Попытка сделать то же самое в Corporation, плачевно заканчивается наследованием нескольких базовых классов McDougles. На этом компилятор выдаст вам сообщение о том что “ McDougles is inaccessible because [it is] also in Ted” (McDougles недоступен, поскольку он уже вTed).

Когда вы получите подобное предупреждение в сложном множестве взаимосвязанных классов, попытайтесь идентифицировать те базовые классы, ля которых необходимо только один объект. В случае с Corporation имеет смысл иметь только одну компанию McDougles. Даже несмотря на то, что Ted выводится из McDougles, это тот же McDougles, который пытается наследовать Corporation. В процесс приобретения активов Теда Corporation не определяется с двумя отдельными компаниями-родителями McDougles. Присутствует лишь один такой родитель, имеющий непосредственное отношение к Corporation и косвенное , к ней же, через Ted.

В похожих (однако менее фривольных) ситуациях, когда в классах, наследующих несколько других, требуется только одна копия множественно наследованного базового класса, вы можете одним махом сократить число объектов этого базового класса до одного-единственного, объявив его виртуальным. В иерархии классов существует только одна копия объекта виртуального базового класса даже в том случае, если класс этого объекта наследуется более одного раз.

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

Скомпилируйте CONGLOM как EasyWin-приложение из интегрированной среды, так как в следующем разделе используются средства оболочки для исследования родства между кассами и программе. Если вы скомпилируете программу из командной строки DOS, вы не сможете или воспользоваться.

Листинг 3.9 CONGLOM.CPP (подключение базовых классов)

1: // conglom.cpp

2:

3: #include <iosream.h>

4: #include <string.h>

5:

6: class Company {

7: private:

8: char *name

9: public:

10: Company(const char *s)

11: name = strdup(s);

12: cout << “In constructor for”;

13: Display();

14: }

15: virtual ??company() {

16: cout << “In destructor for “;

17: Display();

18: delete name;

19: }

20: void Display(void) {cout << name << ‘\n’; }

21: };

22:

23: class Jennys; public Company {

24: public:

25: Jennys(); Company(”Jenny’s”) {}

26: };

27:

28: class McDougles: public Company {

29: public:

30: McDougles(): Company(“McDougles”) {}

31: };

32:

33: class BurgerQueen: public Company {

34: public:

35: BurgerQueen(): Company(“BurgerQueen”) {}

36: };

37:

38: class Bob

39: : virtual public Jennys

40: virtual public McDougles {

41: };

42:

43: class Ted

44: : virtual public McDougles

45: virtual public BurgerQueen {

46: };

47:

48: class Alice

49: : virtual public Jennys,

50: virtual public McDougles,

51: virtual public BurgerQueen {

52: };

53:

54: class Corporation

55: : virtual public McDougles,

56: public Ted,

57: public Alice {

58: private:

59: char *name;

60: public:

61: Corporation(): McDougles(), Ted(), Alice()

62: {name = “Conglomerate Industries”; }

63: void Display(void) { cout << name << ’\n’ ; }

64: };

65:

66: main()

67: {

68: cout << “\nForming a corporation\n”;

69: corporation *cp;

70: cp = new corporation;

71: cp->Display();

72: delete cp;

73: return 0;

74: }

Класс Company не изменился, в то время как в Jennys, McDougles, BurgerQueen, Bob, Ted и Alice, конечно, потребовались изменения для предупреждения в будущем наследования нескольких копий их базового класса-родителя. Например, Bob теперь объявлен следующим образом:

class Bob

: virtual public Jennys

virtual public McDougles {

};

Добавление ключевого слова virtual к перечисленным базовым классам указывает компилятору, что в последующих выводах классов, также наследующих Jennys или McDougles, должна существовать только одна копия этих двух базовых классов. Аналогично, в классах Ted и Alice (строки 53-52) ,базовые класса указаны виртуальными.

Теперь можно спроектировать класс Corporation (54-64). Как и ранее, класс наследует базовые классы McDougles, Ted и Alice. Конечно, с объявлением McDougles виртуальным базовым классом в итоге будет существовать только одна копия класса Corporation, не смотря на то, что Ted и Alice также выведены из класса McDougles.

Замечание

Ted и Alice также могли бы быть объявлены виртуальными базовыми классами в строках 56-57, это было бы необходимо только в том случае , если бы в последующих выводах из класса Corporation также наследовались бы и эти классы.

Использование Borland ObjectBrowser

К сожалению, нет ничего, кроме нескольких рисунков, что помогло бы разъяснить родственные взаимоотношения в множестве производных классов. Но если у вас установлена Microsoft Windows, вы можете использовать ObjectBrowser в интегрированной среде для отображения связанных классов и их членов. Вы сможете пользоваться им независимо от того, пишете ли вы программы для Windows или же для DOS.

ObjectBrowser прост в использовании, но доступен только в интегрированной среде. после компиляции CONGLON в виде EasyWin-приложения (если у вас возникли проблемы, обратитесь к главу 2) следует выбрать View\Class для просмотра графической схемы классов программы и их взаимосвязей (рис. 3.14).

Поскольку в модуле CONGLOM также используются и потоки ввода-вывода для отображения, на схеме показаны ios, streambuf и другие классы из стандартных библиотек, поставляемых вместе с Borland C++.

Двойной щелчок мыши на любом классе (например, McDougles) приведет к открытию окна, демонстрирующего содержимое класса (рис. 3.15), вы можете затем выбрать любую строку в окне для просмотра ее определения. Также попробуйте выбрать фильтры – F (функции), V (переменные), I (наследованные объявления) и V (виртуальные функции) для изменения вида информации, отображаемой в окне.

Отличия структур и объединений от классов

Структуры (struct) и объединения (union) представляют собой частные случаи классов.

Структуры отличаются от классов тем, что доступ к элементам, а также базовый класс при наследовании по умолчанию считаются public. Структуры предпочтительнее использовать для классов, все элементы которых доступны.

Отличия объединений от классов:

  • доступ в объединениях по умолчанию public, кроме того, в них вообще нельзя явным образом использовать спецификаторы доступа;

  • объединение не может участвовать в иерархии классов;

  • элементами объединения не могут быть объекты, содержащие конструкторы и деструкторы;

  • объединение может иметь конструктор и другие методы, только не статические;

  • в анонимном объединении нельзя описывать методы.

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