virtueller Destruktor



  • Hey Leute,

    ich habe, um genauer zu verstehen wie sich die Position des virtuellen Destruktors in einer Klassenhierarchie auswirkt,
    drei Klassen programmiert. Alle Klasse erben public voneinander und verfügen über einen Standardkonstruktor und entsprechenden Destruktor.

    class Base
    {
        public:
            Base()
            {cout << "Konstruktor der Klasse Basis\n" << endl;}
    
            ~Base()
            {cout << "Destruktor der Klasse Basis\n" << endl;}
    
            //virtual~Base()
            //{cout << "Destruktor der Klasse Basis\n" << endl;}
    
    };
    
    class Derived1 : public Base
    {
        public:
            Derived1() 
            {cout << "Konstruktor der Klasse Derived 1\n" << endl;}
    
            ~Derived1()
            {cout << "Destruktor der Klasse Derived 1\n" << endl;}
    
             //virtual~Derived1()
             //{cout << "Destruktor der Klasse Derived1\n" << endl;}
    };
    
    class Derived2 : public Derived1
    {
        public:
            Derived2()
            {cout << "Konstruktor der Klasse Derived 2\n" << endl;}
    
            ~Derived2()
            {cout << "Destruktor der Klasse Derived 2\n" << endl;}
    
             //virtual~Derived2()
             //{cout << "Destruktor der Klasse Derived2\n" << endl;}
    };
    

    Anschließend bin ich alle möglichen Kombinationen durchgegangen sodass, entweder keiner oder immer ein Destruktor in der Klassenhierarchie virtuell gesetzt waren. Für jede Kombination habe ich 3 unterschiedlichen Basisklassenzeiger
    programmiert, welche ich jeweils delete

    Base* ptr = new Derived1; //a d.h. 1. Basisklassenzeiger
    delete ptr;
    Base* ptr = new Derived2; //b d.h. 2. Basisklassenzeiger 
    delete ptr;
    Derived1* ptr = new Derived2; //c d.h. 3. Basisklassenzeiger
    delete ptr;
    

    Dabei habe ich folgende Konsolenausgaben erhalten:

    1. kein Destruktor virtuell
      1a. : "Destruktor der Klasse Basis"
      1b.: "Destruktor der Klasse Basis"
      1c.: "Destruktor der Klasse Derived1
      Destruktor der Klasse Basis"

    2. Destruktor Klasse Base virtuell
      2a.: "Destruktor der Klasse Derived1
      Destruktor der Klasse Basis"
      2b.: "Destruktor der Klasse Derived2
      Destruktor der Klasse Derived1
      Destruktor der Klasse Basis"
      2c.: "Destruktor der Klasse Derived2
      Destruktor der Klasse Derived1
      Destruktor der Klasse Basis"

    3. Destruktor der Klasse Derived1 virtuell
      3a. "Destruktor der Klasse Basis" =>Abbruch mit Fehler
      3b.: "Destruktor der Klasse Basis" =>Abbruch mit Fehler
      3c.: "Destruktor der Klasse Derived2
      Destruktor der Klasse Derived1
      Destruktor der Klasse Basis"

    4. Destruktor der Klasse Derived2 virtuell#
      4a.: "Destruktor der Klasse Basis"
      4b.: "Destruktor der Klasse Basis" => Abbruch mit Fehler
      4c.: "Destruktor der Klasse Derived1
      Destruktor der Klasse Basis"

    Mir ist bewusst, dass in einer Klassenhierarchie, wo Basisklassenzeiger zur Verwaltung dynamischer erzeugter Objekte im Spiel sind, der Destruktor der Basisklasse immer virtuell sein sollte. Wobei die Eigenschaft virtual in einer Klassenhierarchie ja immer automatisch weitervererbt wird. Weshalb im Fall 2 die Destruktoren von der abgeleiteten Klasse in der Hierarchie aufsteigend bis zur Basisklasse aufgerufen werden. Ist die Eigenschaft virtual bei keinen Destruktor vorhanden wird immer der Destruktor der jeweiligen "Basisklasse" aufgerufen, weshalb in Fall 1a und 1b der Destruktor der Base aufgerufen wird. Warum in Fall 1c auch noch der Destruktor der Base auferufen wird. Eigentlich ist in diesen Fall doch Derived1 die "Basisklasse" dessen Destruktor über delete ptr angesprochen wird?
    Des weiteren ist im Fall 3 und 4 manchmal alle Destruktoren aufgerufen werden, dann wieder nur der der Base? Vielen Dank!



  • Des weiteren ist im Fall 3 und 4 manchmal alle Destruktoren aufgerufen werden, dann wieder nur der der Base...warum?
    Insgesamt konnte ich darin keine Logik erkennen. Danke!


  • Mod

    Wenn man normale Aussagen mit einem Fragezeichen beendet, ist nicht klar, was die Frage sein soll?



  • @C-Sepp sagte in virtueller Destruktor:

    Weshalb im Fall 2 die Destruktoren von der abgeleiteten Klasse in der Hierarchie aufsteigend bis zur Basisklasse aufgerufen werden. Ist die Eigenschaft virtual bei keinen Destruktor vorhanden wird immer der Destruktor der jeweiligen "Basisklasse" aufgerufen, weshalb in Fall 1a und 1b der Destruktor der Base aufgerufen wird. Warum in Fall 1c auch noch der Destruktor der Base auferufen wird.

    Es wird nicht der Destruktur der "Basisklasse" aufgerufen, sondern der des statischen Typs. Mit Derived1* ptr sagst du, dass ptr den statischen Typ "Pointer auf Derived1" hat. Der Kompiler weiß also, dass hier also ein Derived1-Objekt zerstört werden soll und ruft dementsprechend die Destruktoren von Derived1 und dann von Base auf.

    Mit virtual würde nicht mehr der statische Typ, sondern der dynamische Typ entscheiden, welcher Destruktor aufgerufen wird.

    Eigentlich ist es aber ganz einfach: es ist undefiniertes Verhalten, wenn du kein virtual im Basis-Destruktor hast und du ein delete auf einen Zeiger auf die Basisklasse aufrufst, der aber auf ein Objekt einer abgeleiteten Klasse zeigt.



  • @C-Sepp
    Vonwegen virtual...

    Wenn du über einen Zeiger vom Typ T löscht, dann ist nur ausschlaggebend ob der Destruktor in der Klasse T virtual ist. (Entweder weil T selbst einen Destruktor hat der als virtual markiert wurde, oder weil T eine Basisklasse mit einem virtuellen Destruktor hat.)

    Wenn T nun einen virtuellen Destruktor hat, dann wird der Destruktor des "dynamic type" aufgerufen - also der Destruktor von dem Typ den das Objekt wirklich hat.

    Und wenn T einen nicht-virtuellen Destruktor hat, dann wird der Destruktor des "static type" aufgerufen, also der von T.

    Und was die Sache mit "warum in Fall 1c auch noch der Destruktor der Base auferufen wird" etc. angeht...

    Das hat mit virtual nichts zu tun. Ein C++ Destruktor macht grundsätzlich folgendes (Fälle wo Exceptions fliegen und virtuelle Vererbung mal ausser Acht gelassen):

    1. Abarbeiten des eigenen Destruktor-Body
    2. Destruktoren aller Member aufrufen (rückwärts, d.h. die zuletzt definierten zuerst)
    3. Destruktoren aller Basisklassen aufrufen (rückwärts, d.h. die letzten in der Basisklassenliste zuerst)

    Wenn du eine Hierarchie mit mehr Ebenen hast, z.B. Base -> Derived -> MoreDerived, dann:

    • Du löscht MoreDerived -> Aufruf von MoreDerived::~MoreDerived
    • MoreDerived::~MoreDerived ruft als letzten Schritt Derived::~Derived auf
    • Derived::~Derived ruft ruft als letzten Schritt Base::~Base auf

    Und genau das siehst du dann im Output wenn du in allen 3 Destruktoren eine Meldung ausgibst.

    Kannst du dir ca. so vorstellen (hier mit nur einer Ebene, dafür 2 Basisklassen):

    struct Base1 {
        // ...
    };
    struct Base2 {
        // ...
    };
    
    struct Derived : Base1, Base2 {
        ~Derived();
        std::string s1;
        std::string s2;
    };
    
    Derived::~Derived() {
        // eigener code:
        std::cout << "~Derived\n";
    
        // vom compiler eingefügt:
        this->s2.~string();
        this->s1.~string();
        this->~Base2();
        this->~Base1();
    }
    


  • Super...vielen Dank!


Anmelden zum Antworten