20.6 Wirtualne destruktory

Aby zapewnić właściwe usuwanie obiektów, należy deklarować destruktor jako funkcję wirtualną, jeśli tylko mamy zamiar korzystać z publicznego dziedziczenia z danej klasy. Wywołując wtedy destruktor (poprzez operator delete) obiektu klasy pochodnej wskazywanego przez wskaźnik do klasy bazowej, wywołamy naprawdę destruktor dla całego obiektu. Dzięki polimorfizmowi wywołany będzie bowiem wtedy destruktor z klasy pochodnej, a destruktor podobiektu klasy bazowej będzie potem wywołany i tak, według normalnych zasad kolejności wywoływania destruktorów. Jak bowiem mówiliśmy, przy usuwaniu obiektu klasy pochodnej najpierw wykonywany jest destruktor dla części „własnej”, a potem automatycznie wywoływany jest destruktor dla podobiektu klasy bazowej. Gdyby destruktor nie był wirtualny, to ponieważ typem statycznym jest wskaźnik do obiektu klasy bazowej, wywołany byłby od razu destruktor z tej klasy, który jednak nie wie na przykład o istnieniu składowych czy zasobów dodanych w klasie pochodnej.

W poniższym przykładzie obiekt klasy pochodnej Pelne jest wskazywany przez wskaźnik osoba typu Nazwisko*, a więc wskaźnik do obiektu klasy bazowej Nazwisko.


P170: wirdes.cpp     Wirtualny destruktor

      1.  #include <iostream>
      2.  using namespace std;
      3.  
      4.  class Nazwisko {
      5.      char* nazwis;
      6.  public:
      7.      Nazwisko(const char* n)
      8.          : nazwis(strcpy(new char[strlen(n)+1], n))
      9.      {
     10.          cout << "Ctor Nazwisko: " << nazwis << endl;
     11.      }
     12.  
     13.      virtual
     14.      ~Nazwisko() {
     15.          cout << "Dtor Nazwisko: " << nazwis << endl;
     16.          delete [] nazwis;
     17.      }
     18.  };
     19.  
     20.  class Pelne : public Nazwisko {
     21.      char* imie;
     22.  public:
     23.      Pelne(const char* i, const char* n)
     24.          : Nazwisko(n),
     25.            imie(strcpy(new char[strlen(i)+1], i))
     26.      {
     27.          cout << "Ctor Pelne, Imie: " << imie << endl;
     28.      }
     29.  
     30.      ~Pelne() {
     31.          cout << "Dtor Pelne, Imie: " << imie << endl;
     32.          delete [] imie;
     33.      }
     34.  };
     35.  
     36.  int main() {
     37.      Nazwisko* osoba = new Pelne("Jan", "Malinowski");
     38.      delete osoba;
     39.  }

Dzięki wirtualności destruktora, podczas usuwania obiektu (linia 38) będzie wywołany najpierw destruktor z rzeczywistej klasy obiektu, a więc z klasy Pelne. Zwolni on pamięć zajmowaną przez imię, a następnie, automatycznie, zadziała destruktor dla podobiektu klasy bazowej Nazwisko, który zwolni pamięć zajmowaną przez nazwisko:
    Ctor Nazwisko: Malinowski
    Ctor Pelne, Imie: Jan
    Dtor Pelne, Imie: Jan
    Dtor Nazwisko: Malinowski
Gdyby destruktor w klasie bazowej Nazwisko nie był zadeklarowany jako wirtualny (po wykomentowaniu linii 13), to wywołany byłby tylko destruktor z klasy określanej przez statyczny typ wskaźnika, a więc z klasy Nazwisko:
    Ctor Nazwisko: Malinowski
    Ctor Pelne, Imie: Jan
    Dtor Nazwisko: Malinowski
i, jak widać, imię w ogóle nie zostałoby usunięte!

Zauważmy, że mamy tu do czynienia z pewną niekonsekwencją: destruktor w klasie pochodnej, ˜Full, przesłania destruktor z klasy bazowej, ˜Name, choć ich nazwy są inne. Pod tym względem destruktor jest wyjątkowy: w innych przypadkach metoda przesłaniająca musi oczywiście mieć tę samą nazwę co metoda przesłaniana.

T.R. Werner, 23 lutego 2019; 23:59