Pomyślałem, że korzystne byłoby omówienie „niezdefiniowanego” zachowania lub przynajmniej „awarii” niezdefiniowanego zachowania, które może wystąpić podczas usuwania przez klasę podstawową (/ struct) bez wirtualnego destruktora, a ściślej bez vtable. Poniższy kod zawiera kilka prostych struktur (to samo dotyczy klas).
#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;
*/
}
Nie sugeruję, czy potrzebujesz wirtualnych niszczycieli, czy nie, choć ogólnie uważam, że dobrą praktyką jest ich posiadanie. Podaję tylko powód, dla którego możesz zakończyć się awarią, jeśli twoja klasa podstawowa (/ struct) nie ma vtable, a twoja klasa pochodna (/ struct) ma i usuwasz obiekt za pomocą klasy podstawowej (/ struct) wskaźnik. W takim przypadku adres, który przekazujesz wolnej procedurze stosu, jest nieprawidłowy, a zatem przyczyna awarii.
Jeśli uruchomisz powyższy kod, zobaczysz wyraźnie, kiedy wystąpi problem. Kiedy ten wskaźnik klasy bazowej (/ struct) różni się od tego wskaźnika klasy pochodnej (/ struct), napotkasz ten problem. W powyższej próbce struktury a i b nie mają vtables. Struktury C i D mają vtables. W ten sposób wskaźnik a lub b do instancji obiektu ac lub d zostanie ustalony, aby uwzględnić tabelę vtable. Jeśli przekażesz ten wskaźnik a lub b, aby go usunąć, nastąpi awaria z powodu nieprawidłowego adresu wolnej procedury sterty.
Jeśli planujesz usunąć instancje pochodne, które mają vtable ze wskaźników klas bazowych, musisz upewnić się, że klasa bazowa ma vtable. Jednym ze sposobów na to jest dodanie wirtualnego destruktora, który i tak może być potrzebny do prawidłowego wyczyszczenia zasobów.