Destruktoren (C++)

Ein Destruktor ist eine Memberfunktion, die automatisch aufgerufen wird, wenn das Objekt außerhalb des Gültigkeitsbereichs liegt oder durch einen Aufruf delete oder delete[]eine explizite Zerstörung erfolgt. Ein Destruktor hat denselben Namen wie die Klasse und wird einer Tilde (~) vorangestellt. Beispielsweise wird der Destruktor für die String-Klasse folgendermaßen deklariert: ~String().

Wenn Sie keinen Destruktor definieren, stellt der Compiler einen Standard bereit, und für einige Klassen ist dies ausreichend. Sie müssen einen benutzerdefinierten Destruktor definieren, wenn die Klasse Standard Ressourcen enthält, die explizit freigegeben werden müssen, z. B. Handles für Systemressourcen oder Zeiger auf den Arbeitsspeicher, der freigegeben werden soll, wenn eine Instanz der Klasse zerstört wird.

Betrachten Sie die folgende Deklaration einer String-Klasse.

// spec1_destructors.cpp
#include <string> // strlen()

class String
{
    public:
        String(const char* ch);  // Declare the constructor
        ~String();               // Declare the destructor
    private:
        char* _text{nullptr};
};

// Define the constructor
String::String(const char* ch)
{
    size_t sizeOfText = strlen(ch) + 1; // +1 to account for trailing NULL

    // Dynamically allocate the correct amount of memory.
    _text = new char[sizeOfText];

    // If the allocation succeeds, copy the initialization string.
    if (_text)
    {
        strcpy_s(_text, sizeOfText, ch);
    }
}

// Define the destructor.
String::~String()
{
    // Deallocate the memory that was previously reserved for the string.
    delete[] _text;
}

int main()
{
    String str("We love C++");
}

Im vorherigen Beispiel verwendet der Destruktor String::~String den delete[] Operator, um den für die Textspeicherung dynamisch zugewiesenen Speicherplatz zuzuordnen.

Deklarieren von Destruktoren

Destruktoren sind Funktionen mit dem gleichen Namen wie die Klasse, jedoch mit einer vorangestellten Tilde (~).

Mehrere Regeln bestimmen die Deklaration von Destruktoren. Destruktoren:

  • Akzeptieren Sie keine Argumente.
  • Geben Sie keinen Wert (oder void) zurück.
  • Kann nicht als const, volatileoder static. Sie können jedoch für die Zerstörung von Objekten aufgerufen werden, die als const, volatileoder static.
  • Kann als virtualdeklariert werden. Mithilfe von virtuellen Destruktoren können Sie Objekte zerstören, ohne ihren Typ zu kennen – der richtige Destruktor für das Objekt wird mithilfe des Mechanismus der virtuellen Funktion aufgerufen. Destruktoren können auch als reine virtuelle Funktionen für abstrakte Klassen deklariert werden.

Verwenden von Destruktoren

Destruktoren werden aufgerufen, wenn eines der folgenden Ereignisse eintritt:

  • Ein lokales (automatisches) Objekt mit Blockbereich verlässt den Gültigkeitsbereich.
  • Wird delete verwendet, um ein objekt zuzuordnen, das mit new. Die Verwendung von delete[] Ergebnissen führt zu nicht definierten Verhaltensweisen.
  • Wird delete[] verwendet, um ein objekt zuzuordnen, das mit new[]. Die Verwendung von delete Ergebnissen führt zu nicht definierten Verhaltensweisen.
  • Die Lebensdauer eines temporären Objekts endet.
  • Ein Programm endet, und es sind globale oder statische Objekte vorhanden.
  • Der Destruktor wird unter Verwendung des vollqualifizierten Namens der Funktion explizit aufgerufen.

Destruktoren können beliebig Klassenmemberfunktionen aufrufen und auf Klassenmemberdaten zugreifen.

Es gibt zwei Einschränkungen bei der Verwendung von Destruktoren:

  • Sie können die Adresse nicht annehmen.

  • Abgeleitete Klassen erben nicht den Destruktor ihrer Basisklasse.

Reihenfolge der Destruktion

Wenn ein Objekt den gültigen Bereich verlässt oder gelöscht wird, lautet die Reihenfolge der Ereignisse bei seiner vollständigen Zerstörung wie folgt:

  1. Der Destruktor der Klasse wird aufgerufen, und der Text der Destruktorfunktion wird ausgeführt.

  2. Destruktoren für nicht statische Memberobjekte werden in umgekehrter Reihenfolge aufgerufen, in der sie in der Klassendeklaration stehen. Die optionale Elementinitialisierungsliste, die beim Erstellen dieser Elemente verwendet wird, wirkt sich nicht auf die Reihenfolge der Konstruktion oder Zerstörung aus.

  3. Destruktoren für nicht virtuelle Basisklassen werden in umgekehrter Reihenfolge der Deklaration aufgerufen.

  4. Destruktoren für virtuelle Basisklassen werden in umgekehrter Reihenfolge der Deklaration aufgerufen.

// order_of_destruction.cpp
#include <cstdio>

struct A1      { virtual ~A1() { printf("A1 dtor\n"); } };
struct A2 : A1 { virtual ~A2() { printf("A2 dtor\n"); } };
struct A3 : A2 { virtual ~A3() { printf("A3 dtor\n"); } };

struct B1      { ~B1() { printf("B1 dtor\n"); } };
struct B2 : B1 { ~B2() { printf("B2 dtor\n"); } };
struct B3 : B2 { ~B3() { printf("B3 dtor\n"); } };

int main() {
   A1 * a = new A3;
   delete a;
   printf("\n");

   B1 * b = new B3;
   delete b;
   printf("\n");

   B3 * b2 = new B3;
   delete b2;
}
A3 dtor
A2 dtor
A1 dtor

B1 dtor

B3 dtor
B2 dtor
B1 dtor

Virtuelle Basisklassen

Destruktoren für virtuelle Basisklassen werden in umgekehrter Reihenfolge ihrer Darstellung in einem gerichteten azyklischen Diagramm aufgerufen (Durchlauf vom tiefsten Punkt nach oben, von links nach rechts, Postorder-Durchlauf). Die folgende Abbildung stellt ein Vererbungsdiagramm dar.

Inheritance graph that shows virtual base classes.

Fünf Klassen mit der Bezeichnung A bis E werden in einem Vererbungsdiagramm angeordnet. Klasse E ist die Basisklasse von B, C und D. Die Klassen C und D sind die Basisklasse von A und B.

Im Folgenden werden die Klassendefinitionen für die in der Abbildung gezeigten Klassen aufgeführt:

class A {};
class B {};
class C : virtual public A, virtual public B {};
class D : virtual public A, virtual public B {};
class E : public C, public D, virtual public B {};

Um die Reihenfolge zum Löschen der virtuellen Basisklassen eines Objekts vom Typ E zu bestimmen, erstellt der Compiler eine Liste mithilfe des folgenden Algorithmus:

  1. Durchlaufen Sie das Diagramm von links, und beginnen Sie mit dem tiefsten Punkt im Diagramm (in diesem Fall E).
  2. Führen Sie Durchläufe von links aus, bis alle Knoten aufgerufen wurden. Notieren Sie den Namen des aktuellen Knotens.
  3. Rufen Sie erneut den vorherigen Knoten auf (nach unten und rechts), um festzustellen, ob es sich bei dem gemerkten Knoten um eine virtuelle Basisklasse handelt.
  4. Falls der gemerkte Knoten eine virtuelle Basisklasse ist, prüfen Sie anhand der Liste, ob diese bereits erfasst ist. Wenn es sich nicht um eine virtuelle Basisklasse handelt, ignorieren Sie sie.
  5. Wenn sich der gespeicherte Knoten noch nicht in der Liste befindet, fügen Sie ihn am Ende der Liste hinzu.
  6. Durchlaufen Sie das Diagramm nach oben und entlang des nächsten Pfads nach rechts.
  7. Fahren Sie mit Schritt 2 fort.
  8. Wenn der letzte nach oben zeigende Pfeil angezeigt wird, notieren Sie den Namen des aktuellen Knotens.
  9. Fahren Sie mit Schritt 3 fort.
  10. Setzen Sie diesen Vorgang fort, bis der untere Knoten wieder der aktuelle Knoten ist.

Daher lautet für die Klasse E die Reihenfolge der Löschung wie folgt:

  1. Die nicht virtuelle Basisklasse E.
  2. Die nicht virtuelle Basisklasse D.
  3. Die nicht virtuelle Basisklasse C.
  4. Die virtuelle Basisklasse B.
  5. Die virtuelle Basisklasse A.

Dieser Prozess erzeugt eine sortierte Liste mit eindeutigen Einträgen. Kein Klassenname wird zweimal angezeigt. Nachdem die Liste erstellt wurde, wird sie in umgekehrter Reihenfolge geführt, und der Destruktor für jede der Klassen in der Liste wird von der letzten bis zur ersten aufgerufen.

Die Reihenfolge der Konstruktion oder Zerstörung ist in erster Linie wichtig, wenn Konstruktoren oder Destruktoren in einer Klasse darauf angewiesen sind, dass die andere Komponente zuerst erstellt oder länger erstellt wird , z. B. wenn der Destruktor für A (in der abbildung zuvor gezeigten Abbildung) weiterhin B vorhanden ist, wenn der Code ausgeführt wird oder umgekehrt.

Folglich sind diese gegenseitigen Abhängigkeiten zwischen den Klassen in einem Vererbungsdiagramm grundsätzlich gefährlich, da später abgeleitete Klassen den am weitesten links stehenden Pfad ändern und somit auch die Reihenfolge zum Erstellen und Löschen verändern können.

Nicht virtuelle Basisklassen

Die Destruktoren für nicht virtuelle Basisklassen werden in umgekehrter Reihenfolge aufgerufen, in der die Basisklassennamen deklariert werden. Betrachten Sie die folgende Klassendeklaration:

class MultInherit : public Base1, public Base2
...

Im vorherigen Beispiel wird der Destruktor für Base2 vor dem Destruktor für Base1 aufgerufen.

Explizite Destruktoraufrufe

Einen Destruktor explizit aufzurufen, ist selten notwendig. Allerdings kann es hilfreich sein, eine Bereinigung von Objekten auszuführen, die an den absoluten Adressen platziert werden. Diese Objekte werden häufig mithilfe eines benutzerdefinierten new Operators zugeordnet, der ein Platzierungsargument verwendet. Der delete Operator kann diesen Speicher nicht umstellen, da er nicht aus dem kostenlosen Speicher zugeordnet ist (weitere Informationen finden Sie unter Den neuen Operatoren und Löschoperatoren). Ein Aufruf des Destruktors kann jedoch eine geeignete Bereinigung ausführen. Mit einer der folgenden Anweisungen können Sie den Destruktor für ein Objekt s der Klasse String explizit aufrufen:

s.String::~String();     // non-virtual call
ps->String::~String();   // non-virtual call

s.~String();       // Virtual call
ps->~String();     // Virtual call

Die Notation für explizite Aufrufe von Destruktoren (zuvor gezeigt) kann unabhängig davon verwendet werden, ob der Typ einen Destruktor definiert. Dies ermöglicht Ihnen solche expliziten Aufrufe, ohne zu wissen, ob ein Destruktor für den Typ definiert ist. Ein expliziter Aufruf von einem Destruktor, wenn keiner definiert ist, hat keine Auswirkungen.

Stabile Programmierung

Eine Klasse benötigt einen Destruktor, wenn sie eine Ressource erwirbt, und um die Ressource sicher zu verwalten, muss sie wahrscheinlich einen Kopierkonstruktor und eine Kopierzuweisung implementieren.

Wenn diese speziellen Funktionen nicht vom Benutzer definiert werden, werden sie implizit vom Compiler definiert. Die implizit generierten Konstruktoren und Zuordnungsoperatoren führen eine flache, memberweise Kopie durch, was fast falsch ist, wenn ein Objekt eine Ressource verwaltet.

Im nächsten Beispiel wird der implizit generierte Kopierkonstruktor die Zeiger str1.text machen und str2.text auf denselben Speicher verweisen, und wenn wir von copy_strings()diesem zurückgeben, wird dieser Speicher zweimal gelöscht, was nicht definiertes Verhalten ist:

void copy_strings()
{
   String str1("I have a sense of impending disaster...");
   String str2 = str1; // str1.text and str2.text now refer to the same object
} // delete[] _text; deallocates the same memory twice
  // undefined behavior

Durch das explizite Definieren eines Destruktors, eines Kopierkonstruktors oder eines Kopierzuweisungsoperators wird die implizite Definition des Verschiebungskonstruktors und des Verschiebungszuweisungsoperators verhindert. In diesem Fall ist das Fehlschlagen von Verschiebungsvorgängen in der Regel, wenn das Kopieren teuer ist, eine verpasste Optimierungschance.

Siehe auch

Kopierkonstruktoren und Kopierzuweisungsoperatoren
Bewegungskonstruktoren und Bewegungszuweisungsoperatoren