Введение
Виртуальный деструктор — одна из тех тем C++, которые кажутся мелочью, пока не приводят к утечке памяти или странному поведению программы. Он нужен, когда объект производного класса удаляется через указатель на базовый класс. В статье на простых примерах разберём, в каком порядке вызываются конструкторы и деструкторы, что происходит без ключевого слова virtual и как правильно объявлять деструкторы в иерархиях классов, в том числе в Qt.
Порядок вызова конструкторов и деструкторов
Рассмотрим два класса: базовый AA и производный BB. Каждый конструктор и деструктор выводит сообщение:
class AA {
public:
AA() { qDebug() << "AA()"; }
~AA() { qDebug() << "~AA()"; }
};
class BB : public AA {
public:
BB() { qDebug() << "BB()"; }
~BB() { qDebug() << "~BB()"; }
};
Создадим объект на стеке:
int main()
{
BB b;
return 0;
}
Порядок вызовов будет следующим:
AA()
BB()
// При выходе из main:
~BB()
~AA()
Сначала создаётся базовая часть объекта, затем производная. При разрушении всё происходит в обратном порядке: сначала вызывается деструктор производного класса, а по его окончании — деструктор базового. Здесь всё работает правильно, потому что компилятор точно знает тип объекта.
Проблема: удаление через указатель на базовый класс
Теперь создадим объект в динамической памяти, но сохраним указатель на него в переменной базового типа. Так часто делают при работе с полиморфизмом:
int main()
{
AA *b = new BB;
delete b;
return 0;
}
Вывод программы:
AA()
BB()
// При вызове delete b:
~AA()
Деструктор ~BB() не вызвался. По стандарту C++ удаление производного объекта через указатель на базовый класс без виртуального деструктора — это неопределённое поведение. На практике чаще всего вызывается только деструктор базового класса. Если производный класс владел ресурсами (памятью, файлом, сокетом), они не будут освобождены.
class BB : public AA {
public:
BB() : m_data(new int[1000000]) {}
~BB() { delete[] m_data; } // не будет вызван!
private:
int *m_data;
};
Решение: виртуальный деструктор
Добавим слово virtual к деструктору базового класса:
class AA {
public:
AA() { qDebug() << "AA()"; }
virtual ~AA() { qDebug() << "~AA()"; }
};
Перезапустим программу:
AA()
BB()
// При вызове delete b:
~BB()
~AA()
Теперь вызывается деструктор реального типа объекта, а затем по цепочке — деструктор базового. Виртуальный деструктор работает так же, как любая виртуальная функция: нужная версия выбирается во время выполнения по фактическому типу объекта.
Как правильно объявлять деструкторы
- Если у класса есть хотя бы одна виртуальная функция, сделайте деструктор виртуальным.
- Если деструктору нечего делать, используйте
virtual ~AA() = default;. - В производных классах отмечайте деструктор словом
override:~BB() override;. Компилятор проверит, что базовый деструктор действительно виртуальный. - Если класс не предназначен для полиморфного удаления, можно сделать деструктор защищённым и невиртуальным. Тогда удалить объект через указатель на базовый класс просто не получится.
- Классы, от которых нельзя наследоваться, помечайте словом
final.
Компиляторы GCC и Clang умеют предупреждать о проблеме: включите флаги -Wnon-virtual-dtor и -Wdelete-non-virtual-dtor.
Умные указатели не отменяют правило
Умные указатели не отменяют правило. std::unique_ptr<AA>, хранящий объект BB, при удалении вызовет delete через указатель на AA, и без виртуального деструктора возникнет та же проблема. std::shared_ptr, созданный через std::make_shared<BB>(), запоминает правильный способ удаления, но полагаться на это не стоит.
Деструкторы в Qt: класс QObject
У класса QObject деструктор уже виртуальный. Поэтому все классы, унаследованные от QObject или QWidget, корректно удаляются через указатель на базовый класс, и объявлять деструктор заново не обязательно. Именно благодаря этому работает система владения объектами в Qt: родитель удаляет дочерние объекты через указатели на QObject, и у каждого вызывается деструктор его настоящего класса.
Цена виртуального деструктора
Такой деструктор добавляет в объект указатель на таблицу виртуальных функций. Обычно это 8 байт на 64-битной системе. Если в классе уже есть виртуальные функции, дополнительных затрат нет. Для простых структур данных без наследования он не нужен.
Заключение
Виртуальный деструктор необходим, когда объекты производных классов удаляются через указатель на базовый класс. Без него вызывается только деструктор базового класса, что приводит к утечкам ресурсов и неопределённому поведению. Простое правило: есть виртуальные функции — сделайте деструктор виртуальным. В Qt это уже сделано за вас в классе QObject.