Виртуальные деструкторы
Совет
Public
Чистые виртуальные функции-члены
Чистая виртуальная функция-член – пустое место, ожидающее своего заполнения производным классом.
Объявляется чистая виртуальная функция точно так же, как и обычная, только ее объявление в классе завершается = 0.
например
class AbstractClass {
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:
19virtual int CompareElems(PTElem p1, PTElem p2) = 0;
20: public:
21: TSet(int n)
22: {max = n; index = 0; set = newPTElem [n];}
23: virtual ~TSet()
24: {delete[] set; }
25: void AddElem(PTElem p);
26: int HasElem(PTElemp);
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 если класс, производный от абстрактного, не определяет все чисто виртуальные функции, он также является абстрактным.
Таким образом, можно создать функцию, параметром которой является указатель на абстрактный класс. На место этого параметра при выполнении программы может передаваться указатель на объект любого производного класса.
Это позволяет создавать полиморфные функции, работающие с объектом любого типа в пределах одной иерархии.