Rambler's Top100 Service калинин.ru / программирование / c и c++ /  << 28.07.00 >>

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

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

Итак, рассмотрим маленький пример:

class A
{
public:
  virtual void f() = 0;
  ~A();
};

class B : public A
{
public:
  virtual void f();
  ~B();
};

Вызов компилятора gcc строкой

g++ -c -Wall test.cpp
Даст следующий результат:

test.cpp:6: warning: `class A' has virtual functions but non-virtual destructor
test.cpp:13: warning: `class B' has virtual functions but non-virtual destructor

Это только предупреждения, компиляция прошла вполне успешно. Тем не менее, почему же gcc выдает подобные предупреждения?

Все дело в том, что виртуальные функции используются в C++ для обеспчения полиморфизма --- т.е., клиентская функция вида:

void call_f(A* a)
{
  a->f();
}

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

std::vector<A*> a_collection;
a_collection.push_back(new B());

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

for(std::vector<A*>::iterator i = ... )
  delete *i;

все объекты, содержащиеся в a_collection, будут удалены так, как будто это --- объекты класса A.

В этом можно убедиться, если соответствующим образом определить деструкторы классов A и B:

inline A::~A()
{
  puts("A::~A()");
}

inline B::~B()
{
  puts("B::~B()");
}

Тогда выполнение следующего кода:

A* ptr = new B();
delete ptr;

Приведет к следующему результату:

A::~A()

Если же в определении класса A деструктор был бы сделан виртуальным (virtual ~A();), то результат был бы иным:

B::~B()
A::~A()

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

#include <stdio.h>

class A
{
public:
  A(const char* n);
  ~A();
protected:
  const char* name;
};

inline A::A(const char* n) : name(n)
{ }
inline A::~A()
{
  printf("A::~A() for %s.\n", name);
}

class B
{
public:
  virtual void f();
  B();
  ~B();
protected:
  A a1;
};

inline B::~B()
{ }

inline B::B() : a1("a1")
{ }

void B::f() { }

class C : public B
{
public:
  C();
protected:
  A a2;
};

inline C::C() : a2("a2")
{ }

int main()
{
 B* ptr = new C();
 delete ptr;
 return 0;
}

Компиляция этого примера проходит без ошибок (но с предупреждениями), вывод программы следующий:

A::~A() for a1.

Немного не то, что ожидалось? Тогда поставим перед названием деструктора класса B слово virtual. Результат изменится:

A::~A() for a2.
A::~A() for a1.

Теперь вывод программы несколько более соответствует действительности.

Резюме

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


Версия для печати


  Ссылки по теме:
Бъерн Страуструп
   Язык программирования C++, 3 издание.
  Рядом в разделе:
Запись структур данных в двоичные файлы (31.07.00)
   Чтение и запись данных, вообще говоря, одна из самых часто встречающихся операций. Сложно себе представить программу, которая бы совсем не нуждалась...   >>>>
Использование "умных" указателей (27.07.00)
   Принципы использования "умных" указателей известны каждому программисту на C++. Идея предельно проста: вместо того, что бы пользоваться объектами некоторого класса, указателями...   >>>>
  Рядом по дате:
kontrreklama.go.ru, сайт сопротивления тотальному видеовоздействию (29.07.00)
   Сайт с достаточно интересной направленностью --- противодействию рекламы. По мнению авторов сайта, телевизионная реклама представляет из себя угрозу для человека. Почему?...   >>>>
Использование "умных" указателей (27.07.00)
   Принципы использования "умных" указателей известны каждому программисту на C++. Идея предельно проста: вместо того, что бы пользоваться объектами некоторого класса, указателями...   >>>>
  Содержание:
Заглавная страница
Мой блог
Мое резюме
Дайджест
Программирование
   C&C++
Сети
Unix
Алгоритмы
Оптимизация
Соревнования
Отвлеченно
XML
TeX
Просто так
Студенческое
Туризм
  Байки
Фотографии
Комментарии
   Книги
Web-ресурсы
Фильмы
Интернет
Программное обеспечение
Жизнь
Благодарности
Форум
Хронология
 
  В этом разделе:
Простой, но полезный аллокатор памяти (18.02.03)
   Эта заметка --- продолжение "Postfix изнутри" в том смысле, что в качестве примера опять берется postfix. Но если в прошлый раз...   >>>>
C или C++? (09.07.01)
   Существуют два диаметрально противоположенных, но одинаково распространенных мнения, которые можно выразить как "C++ это C с классами" и "C++ и C...   >>>>
Религия и goto (14.04.01)
   Начнем несколько издалека. В программировании существует тенденция к алгоритмизации самого процесса программирования. То есть, выведение некоторых универсальных правил, использование которых в...   >>>>
ploticus (16.10.00)
   Есть такая программа, предназначенная для создания графиков различных видов из командной строки, называется ploticus. Программа сама по себе достаточно удобная ---...   >>>>
Шаманство, или ошибки работы с памятью (25.09.00)
   Когда программа становится внушительной по своему содержанию (то есть, не по количеству строчек, а по непонятности внутренних связей), то ее поведение...   >>>>
Библиотека консорциума W3, libwww (20.09.00)
   Популярный нынче термин "веб-программирование" обычно подразумевает под собой программирование, в лучшем случае, на perl, в худшем --- на PHP, в совсем...   >>>>
Инварианты внутри программы (18.09.00)
   Вы когда-нибудь задумывались, над тем, как вы пишите программы? Если нет, то, я думаю, сегодняшняя заметка будет вам полезна. Итак, как...   >>>>
Содержание раздела полностью...
   Примерно в тоже время
kontrreklama.go.ru, сайт сопротивления тотальному видеовоздействию (29.07.00)
   Сайт с достаточно интересной направленностью --- противодействию рекламы. По мнению авторов сайта, телевизионная реклама представляет из себя угрозу для человека. Почему?...   >>>>
Использование "умных" указателей (27.07.00)
   Принципы использования "умных" указателей известны каждому программисту на C++. Идея предельно проста: вместо того, что бы пользоваться объектами некоторого класса, указателями...   >>>>
Хронология полностью...
   Содержание
Заглавная страница
Мой блог
Мое резюме
Дайджест
Программирование
  C&C++
Сети
Unix
Алгоритмы
Оптимизация
Соревнования
Отвлеченно
XML
TeX
Туризм
  Байки
Фотографии
Комментарии
  Книги
Web-ресурсы
Фильмы
Интернет
Программное обеспечение
Жизнь
Студенческое
Просто так
Благодарности
Форум
Хронология
© 2000-2008, Andrey L. Kalinin
mailto:andrey@kalinin.ru
Rambler's Top100