Виртуальные функции. Что это такое? Часть 3 (исходники)

Источник: Весельчак У
Сергей Малышев (aka Михалыч)

Часть 1 :: Часть 2

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

Этой частью мы завершим начатое в первой и второй частях статьи рассмотрение использования виртуальных функций. Если вы читали серию статей "Классы. Копирование и присваивание", то помните, что там говорилось о том, как создаются объекты классов. Теперь поговорим о том, как они уничтожаются. Правда, следует оговориться сразу. Очень неплохо если вы уже знаете, что такое деструкторы в целом. Поскольку эта теоретическая часть сюда никак не вписывается. Если вы не знакомы с этим, совсем не помешает для начала почитать что-либо о конструкторах и деструкторах классов.

Когда нужны виртуальные деструкторы?

Давайте начнем изучение вопроса с рассмотрения простого (и ставшего уже классическим) примера.

Соорудим некий класс, который может запоминать строковое значение. И пусть он у нас будет базовым классом (правда не абстрактным, так как это не важно в данном случае), из него мы будем выводить другие.
Итак.

Код:

class Base
{
  private:
    char *sp1;
  public:
    Base(const char *S)  { sp1=strdup(S); } //конструктор
    ~Base()              { delete sp1; } //деструктор
};

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

Код:

class Derived: public Base
{
  private:
    char *sp2;
  public:
    Derived(const char *S1, const char *S2): Base(S1)
                { sp2=strdup(S2); } //это был конструктор
    ~Derived()  { delete sp2; }     //а это уже деструктор
};

Этот класс сохраняет вторую строку, на которую ссылается его указатель sp2. Новый конструктор вызывает конструктор базового класса, передавая строку в базовый класс, а также выделяет память под вторую строку и сохраняет адрес новой строки в указателе sp2. Деструктор этого класса освобождает эту память.
Теперь где-то в программе мы можем создать объект такого класса:

Код:

Derived MyStrings(string 1, string 2);

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

Рассмотрим другой вариант. Предположим, что мы объявили указатель на базовый класс Base, но присвоили ему адрес объекта класса Derived. Это вполне допустимо, мы уже обсуждали этот вопрос в предыдущих частях. То есть, это будет выглядеть в программе так:

Код:

Base *pBase; //указатель на базовый класс
pBase=new Derived(string 1, string 2);

Что же произойдет, когда в программе будет удален объект, на который ссылается указатель pBase?

Код:

delete pBase; //?????????????

Компилятор видит, что указатель pBase должен ссылаться на объекты класса Base (откуда бы ему узнать, что именно присвоено этому указателю?). И вполне естественно программа вызовет только деструктор базового класса, и он удалит одну строку, но оставит в памяти другую. Ведь деструктор класса Derived не вызывался! Получается классическая утечка памяти!

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

Код:

virtual ~Base()     { delete sp1; } //деструктор
virtual ~Derived()  { delete sp2; } //деструктор

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

Ну и, пожалуй, последнее.
Может так случиться, что в некоторых случаях будет очень удобно определить в классе чистый виртуальный деструктор. Помните чистые виртуальные функции? Они дают нам абстрактные классы, от которых невозможно создать объект. Это основа для построения иерархии классов. Однако, иногда встречаются классы, которые имело бы смысл сделать абстрактными, но для этого в вашем распоряжении может не оказаться чистых виртуальных функций. Как быть? Решение не сложное. Объединим понятие чистой виртуальной функции и виртуального деструктора. Надо просто объявить в классе, который должен быть абстрактным, чистый виртуальный деструктор.
Приведем пример. Допустим, это будет класс, описывающий нечто непонятное.

Код:

class Something //абстрактный класс без виртуальных функций
{
   public:
    virtual ~Something()=0; //а это чистый виртуальный деструктор
};

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

Код:

Something::~Something() {};

Это необходимо сделать, поскольку виртуальный деструктор работает таким образом, что вначале вызывается деструктор производного класса, а затем последовательно деструкторы классов, находящихся выше в цепи наследования, вплоть до базового абстрактного. Это означает, что компилятор будет генерировать вызов ~Something(), даже когда класс является абстрактным, поэтому тело функции надо определять обязательно. Если этого не сделать, компоновщик просто выдаст ошибку отсутствия символа. И сделать это все равно придется.

В завершение сказанного парочка советов:
Если у класса имеются виртуальные функции, имеет прямой смысл создать для него виртуальный деструктор. Даже если он и не требуется этому классу. Классы, которые будут потом произведены от него, может быть будут содержать деструкторы, которые должны вызываться соответствующим образом. Если же класс не содержит виртуальных функций, то скорее всего он не предполагается к использованию в качестве базового. В таком случае определение в нем виртуального деструктора обычно неоправданно.
И еще. Что называется с разбега можно совершить очень интересную ошибку. Просто по аналогии с деструктором  объявить конструктор виртуальным. Конструкторы не могут быть виртуальными. Будьте бдительны!

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


Страница сайта http://test.interface.ru
Оригинал находится по адресу http://test.interface.ru/home.asp?artId=6284