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

Практически всегда деструктор делается виртуальным. Делается это для того, чтобы корректно (без утечек памяти) уничтожались объекты не только заданного класса, но и любого производного от него.

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

Приведем пример виртуального деструктора.

Пример 9

#include <iostream.h>

class color

{

public:

virtual ~color();   // виртуальный деструктор для color

};

class red : public color

{

public:

~red();             // деструктор для red также является виртуальным

};

class brightred : public red

{

public:

~brightred();       // деструктор для brightred также виртуальный

};

Приведем еще один пример виртуального деструктора.

Пример 10

#include <cstdlib>

#include <iostream.h>

class A {

public:

A() { cout << "A()" << endl; }

~A() { cout << "~A()" << endl; }

};

class B : public A {

public:

B() { cout << "B()" << endl; }

~B() { cout << "~B()" << endl; }

};

int main()

{

B b;

return EXIT_SUCCESS;

}

Результат работы программы представлен на рис. 14.4.

Рис. 14.4. Результат работы программы (пример 10)

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

Пример 11

#include <cstdlib>

#include <iostream.h>

class A

{

public:

A() { cout << "A()" << endl; }

~A() { cout << "~A()" << endl; }

};

class B : public A

{

public:

B() { cout << "B()" << endl; }

~B() { cout << "~B()" << endl; }

};

int main()

{

A * pA = new B;

delete pA;

return EXIT_SUCCESS;

}

Результат работы программы представлен на рис. 14.5.

Рис. 14.5. Результат работы программы (пример 11)

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

Пример 12

#include <cstdlib>

#include <iostream.h>

class A

  {

public:

A() { cout << "A()" << endl; }

virtual ~A() { cout << "~A()" << endl; }

};

class B : public A

{

public:

B() { cout << "B()" << endl; }

~B() { cout << "~B()" << endl; }

};

int main()

{

A * pA = new B;

delete pA;

return EXIT_SUCCESS;

}

Результат работы программы представлен на рис. 14.6.

Рис. 14.6. Результат работы программы (пример 12)

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