Виртуальный деструктор: нужен ли он когда динамически не выделяется память?
Нужен ли нам виртуальный деструктор, если мои классы не распределяют память динамически?
например
class A
{
private:
int a;
int b;
public:
A();
~A();
};
class B: public A
{
private:
int c;
int d;
public:
B();
~B();
};
В этом случае мы должны пометить деструктор А как виртуальный?
6 ответов
Вопрос не в том, выделяют ли ваши классы память динамически. Это если пользователь классов выделяет объект B через указатель A, а затем удаляет его:
A * a = new B;
delete a;
В этом случае, если для A нет виртуального деструктора, стандарт C++ говорит, что ваша программа демонстрирует неопределенное поведение. Это не хорошая вещь.
Это поведение указано в разделе 5.3.5/3 Стандарта (здесь delete
):
если статический тип операнда отличается от его динамического типа, статический тип должен быть базовым классом динамического типа операнда, а статический тип должен иметь виртуальный деструктор, или поведение не определено.
Цель виртуального деструктора (то есть цель сделать виртуальный деструктор) состоит в том, чтобы облегчить полиморфное удаление объектов посредством delete-expression. Если ваш дизайн не требует полиморфного удаления объектов, вам не нужны виртуальные деструкторы. Ссылаясь на ваш пример, если вам когда-нибудь придется удалить объект типа B
через указатель типа A *
(полиморфное удаление), вам понадобится виртуальный деструктор как можно выше в иерархии A
, Вот так это выглядит с формальной точки зрения.
(Заметьте, кстати, как сказал Нил, важно то, как вы создаете / удаляете свои объекты классов, а не как классы управляют своей внутренней памятью.)
Что касается хорошей практики программирования... Это зависит от ваших намерений и вашего дизайна в конце концов. Если ваши классы вообще не предназначены для полиморфизации (никаких виртуальных методов), вам не нужны виртуальные деструкторы. Если ваш класс полиморфен (имеет по крайней мере один виртуальный метод), то сделать виртуальный деструктор "на всякий случай" может быть очень хорошей идеей, и в этом случае он практически не имеет потери производительности / памяти.
Последнее обычно выражается в виде довольно известного руководства по хорошей практике: если в вашем классе есть хотя бы один виртуальный метод, также сделайте деструктор виртуальным. Хотя с формальной точки зрения виртуальный деструктор там может и не понадобиться, это все же довольно хорошее руководство.
Классы, которые не имеют ресурсов, но могут образовывать полиморфные иерархии, должны всегда определять пустые виртуальные деструкторы, за исключением того, что вполне достаточно определить явный пустой (и даже чистый) виртуальный деструктор в самом основании иерархии. Все остальные деструкторы станут виртуальными автоматически, даже если они неявно определены компилятором. Т.е. вам не нужно явно определять пустой деструктор в каждом классе. Просто базы достаточно.
Освобождение памяти - не единственная важная функция, которую может выполнять деструктор. Это может также использоваться, например, для сброса глобального состояния. Невыполнение этого требования не приведет к утечке памяти, но потенциально может вызвать другие проблемы в вашей программе.
Кроме того, даже если ваш деструктор не делает ничего полезного сегодня, это может произойти в какой-то момент в будущем. Нет реальной причины избегать виртуального деструктора, если у вас есть наследство, так почему бы просто не добавить его и спать лучше ночью?
Деструктор родительского класса всегда вызывается автоматически, и dtor по умолчанию всегда генерируется, если явно не объявлен dtor. В вашем примере ни A, ни B не должны иметь нетривиальный dtor.
Если в вашем классе есть виртуальные функции, дополнительный виртуальный dtor не помешает, и это хорошая практика. В случае, если вы выделяете память или любой другой ресурс (например, открываете файл), необходим dtor, чтобы снова освободить этот ресурс после уничтожения.
Цель объявления деструктора как виртуального состоит в том, чтобы иметь возможность вызывать деструктор производного класса всякий раз, когда вы вызываете delete для указателя типа Base, который указывает на объект типа Derived. Невыполнение этого условия приведет к неопределенному поведению.
Предположение, что вам не нужно помечать деструктор как виртуальный, если вы не выделяете память динамически, подразумевает, что вам не нужно вызывать деструктор производного класса, если вы не выделяете память динамически, что неправильно. Поскольку вы все еще можете выполнять несколько других операций в деструкторе вашего производного класса, кроме просто освобождения динамически распределенной памяти. Примерами могут быть закрытие открытого файла, регистрация некоторой информации и т. Д.
Если ваша единственная проблема - память, возможно, вам следует начать с защиты деструктора базового класса (и / или, возможно, других). Тогда, если что-то не скомпилируется, вы поймете, почему. Ref: boost:: любые способы.