Почему я должен объявлять виртуальный деструктор абстрактного класса на С++?
Я знаю, что хорошей практикой является объявление виртуальных деструкторов для базовых классов в С++, но всегда ли важно объявлять деструкторы virtual
даже для абстрактных классов, которые функционируют как интерфейсы? Пожалуйста, предоставьте несколько причин и примеров.
Ответы
Ответ 1
Это еще более важно для интерфейса. Любой пользователь вашего класса, вероятно, будет содержать указатель на интерфейс, а не указатель на конкретную реализацию. Когда они придут, чтобы удалить его, если деструктор не является виртуальным, они вызовут деструктор интерфейса (или предоставленный компилятором по умолчанию, если вы его не указали), а не деструктор производного класса. Мгновенная утечка памяти.
Например
class Interface
{
virtual void doSomething() = 0;
};
class Derived : public Interface
{
Derived();
~Derived()
{
// Do some important cleanup...
}
};
void myFunc(void)
{
Interface* p = new Derived();
// The behaviour of the next line is undefined. It probably
// calls Interface::~Interface, not Derived::~Derived
delete p;
}
Ответ 2
Ответ на ваш вопрос часто, но не всегда. Если ваш абстрактный класс запрещает клиентам вызывать delete по указателю на него (или если он говорит об этом в своей документации), вы можете не объявлять виртуальный деструктор.
Вы можете запретить клиентам вызывать delete по указателю, сделав его деструктором защищенным. Работая так, совершенно безопасно и разумно опустить виртуальный деструктор.
В конечном итоге у вас не останется таблицы виртуальных методов, и в итоге вы дадите сигнал своим клиентам о своем намерении сделать ее не удаляемой через указатель на нее, поэтому у вас действительно есть причина не объявлять ее виртуальной в этих случаях.
[См. Пункт 4 в этой статье: http://www.gotw.ca/publications/mill18.htm ]
Ответ 3
Я решил провести некоторое исследование и попытаться обобщить ваши ответы. Следующие вопросы помогут вам решить, какой деструктор вам нужен:
- Является ли ваш класс предназначен для использования в качестве базового класса?
- Нет: объявить публичный не виртуальный деструктор, чтобы избежать v-указателя на каждый объект класса *.
- Да: прочитайте следующий вопрос.
- Является ли ваш базовый класс абстрактным? (т.е. любые виртуальные чистые методы?)
- Нет. Попробуйте сделать свой абстрактный базовый класс, изменив иерархию классов.
- Да: прочитайте следующий вопрос.
- Вы хотите разрешить полиморфное удаление через базовый указатель?
- Нет: объявить защищенный виртуальный деструктор для предотвращения нежелательного использования.
- Да: объявить публичный виртуальный деструктор (в этом случае нет накладных расходов).
Надеюсь, это поможет.
* Важно отметить, что в С++ нет возможности пометить класс как final (т.е. не подклассы), поэтому в случае, если вы решите объявить свой деструктор не виртуальным и публично, помните, чтобы явным образом предупреждал своих коллег-программистов о выводе из вашего класса.
Литература:
Ответ 4
Это не всегда требуется, но я считаю, что это хорошая практика. Что он делает, так это позволяет безопасно удалить производный объект через указатель базового типа.
Так, например:
Base *p = new Derived;
// use p as you see fit
delete p;
плохо сформирован, если у Base
нет виртуального деструктора, потому что он попытается удалить объект, как если бы он был Base *
.
Ответ 5
Да, это всегда важно. Производные классы могут выделять память или удерживать ссылку на другие ресурсы, которые необходимо будет очистить, когда объект будет уничтожен. Если вы не даете виртуальные деструкторы интерфейсов/абстрактных классов, то каждый раз, когда вы удаляете экземпляр производного класса с помощью дескриптора базового класса, деструктор производного класса не будет вызываться.
Следовательно, вы открываете потенциал утечек памяти
class IFoo
{
public:
virtual void DoFoo() = 0;
};
class Bar : public IFoo
{
char* dooby = NULL;
public:
virtual void DoFoo() { dooby = new char[10]; }
void ~Bar() { delete [] dooby; }
};
IFoo* baz = new Bar();
baz->DoFoo();
delete baz; // memory leak - dooby isn't deleted
Ответ 6
Это не только хорошая практика. Это правило №1 для любой иерархии классов.
- Основной класс иерархии в С++ должен иметь виртуальный деструктор
Теперь для Почему. Возьмите типичную иерархию животных. Виртуальные деструкторы проходят виртуальную отправку, как и любой другой вызов метода. Возьмем следующий пример.
Animal* pAnimal = GetAnimal();
delete pAnimal;
Предположим, что Animal является абстрактным классом. Единственный способ, которым С++ знает правильного деструктора для вызова, - это отправка виртуального метода. Если деструктор не является виртуальным, он просто вызывает деструктор животных и не уничтожает какие-либо объекты в производных классах.
Причиной создания виртуального деструктора в базовом классе является то, что он просто удаляет выбор из производных классов. Их деструктор становится виртуальным по умолчанию.
Ответ 7
Ответ прост, вам нужно, чтобы он был виртуальным, в противном случае базовый класс не был бы полным полиморфным классом.
Base *ptr = new Derived();
delete ptr; // Here the call order of destructors: first Derived then Base.
Вы предпочли бы вышеупомянутое удаление, но если деструктор базового класса не является виртуальным, будет вызван только деструктор базового класса, и все данные в производном классе останутся без изменений.
Ответ 8
Не всегда:
class Interface
{
virtual void doSomething(void) = 0;
// uncomment this if derived classes allocate memory...
// ...or have members with destructors.
//CHECK: virtual ~Interface() {}
};
class Derived : public Interface
{
virtual void doSomething(void) { variable = 100; }
int variable;
};
class DerivedOther : public Interface
{
DerivedOther(const Something &ref) : ref(ref) {}
virtual void doSomething(void) { ref.member = 100; }
Something &ref;
};
void myFunc(void)
{
Interface* p = new Derived();
delete p; // calls Interface::~Interface, not Derived::~Derived (not a problem)
}