Я подумал, что было бы полезно обсудить «неопределенное» поведение или, по крайней мере, «аварийное» неопределенное поведение, которое может возникнуть при удалении через базовый класс (/ struct) без виртуального деструктора или, точнее, без vtable. Код ниже перечисляет несколько простых структур (то же самое будет верно для классов).
#include <iostream>
using namespace std;
struct a
{
~a() {}
unsigned long long i;
};
struct b : a
{
~b() {}
unsigned long long j;
};
struct c : b
{
~c() {}
virtual void m3() {}
unsigned long long k;
};
struct d : c
{
~d() {}
virtual void m4() {}
unsigned long long l;
};
int main()
{
cout << "sizeof(a): " << sizeof(a) << endl;
cout << "sizeof(b): " << sizeof(b) << endl;
cout << "sizeof(c): " << sizeof(c) << endl;
cout << "sizeof(d): " << sizeof(d) << endl;
// No issue.
a* a1 = new a();
cout << "a1: " << a1 << endl;
delete a1;
// No issue.
b* b1 = new b();
cout << "b1: " << b1 << endl;
cout << "(a*) b1: " << (a*) b1 << endl;
delete b1;
// No issue.
c* c1 = new c();
cout << "c1: " << c1 << endl;
cout << "(b*) c1: " << (b*) c1 << endl;
cout << "(a*) c1: " << (a*) c1 << endl;
delete c1;
// No issue.
d* d1 = new d();
cout << "d1: " << d1 << endl;
cout << "(c*) d1: " << (c*) d1 << endl;
cout << "(b*) d1: " << (b*) d1 << endl;
cout << "(a*) d1: " << (a*) d1 << endl;
delete d1;
// Doesn't crash, but may not produce the results you want.
c1 = (c*) new d();
delete c1;
// Crashes due to passing an invalid address to the method which
// frees the memory.
d1 = new d();
b1 = (b*) d1;
cout << "d1: " << d1 << endl;
cout << "b1: " << b1 << endl;
delete b1;
/*
// This is similar to what's happening above in the "crash" case.
char* buf = new char[32];
cout << "buf: " << (void*) buf << endl;
buf += 8;
cout << "buf after adding 8: " << (void*) buf << endl;
delete buf;
*/
}
Я не предполагаю, нужны ли вам виртуальные деструкторы или нет, хотя я думаю, что в общем случае иметь их стоит. Я просто указываю причину, по которой вы можете столкнуться с падением, если ваш базовый класс (/ struct) не имеет vtable, а ваш производный класс (/ struct) имеет, и вы удаляете объект через базовый класс (/ struct) указатель. В этом случае адрес, который вы передаете свободной подпрограмме кучи, является недействительным и, следовательно, причиной сбоя.
Если вы запустите приведенный выше код, вы ясно увидите, когда возникнет проблема. Когда указатель this базового класса (/ struct) отличается от указателя this производного класса (/ struct), вы столкнетесь с этой проблемой. В приведенном выше примере struct a и b не имеют vtables. Структуры C и D имеют Vtables. Таким образом, указатель a или b на экземпляр объекта ac или d будет зафиксирован для учета виртуальной таблицы. Если вы передадите указатель a или b для удаления, произойдет сбой из-за того, что адрес является недопустимым для свободной подпрограммы кучи.
Если вы планируете удалять производные экземпляры, которые имеют vtables из указателей базового класса, вам необходимо убедиться, что базовый класс имеет vtable. Один из способов сделать это - добавить виртуальный деструктор, который в любом случае может потребоваться для правильной очистки ресурсов.