Destruktoren (C++)

Ein Destruktor ist eine Memberfunktion, die automatisch aufgerufen wird, wenn das Objekt außerhalb des Bereichs geht oder durch einen Aufruf deleteexplizit zerstört wird. Ein Destructor hat denselben Namen wie die Klasse, vor einer Tilde (~). Beispielsweise wird der Destruktor für die String-Klasse folgendermaßen deklariert: ~String().

Wenn Sie keinen Destruktor definieren, stellt der Compiler eine Standardeinstellung bereit; für viele Klassen ist dies ausreichend. Sie müssen nur einen benutzerdefinierten Destruktor definieren, wenn die Klasse Systemressourcen speichert, die freigegeben werden müssen, oder Zeiger, die den Speicher besitzen, auf den sie verweisen.

Betrachten Sie die folgende Deklaration einer String-Klasse.

// spec1_destructors.cpp
#include <string>

class String {
public:
   String( char *ch );  // Declare constructor
   ~String();           //  and destructor.
private:
   char    *_text;
   size_t  sizeOfText;
};

// Define the constructor.
String::String( char *ch ) {
   sizeOfText = strlen( ch ) + 1;

   // 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 this string.
   delete[] _text;
}

int main() {
   String str("The piper in the glen...");
}

Im vorhergehenden Beispiel verwendet der Destruktor String::~String den delete Operator, um den Für textspeicher dynamisch zugewiesenen Speicherplatz zu behandeln.

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 keine Argumente.

  • Geben Sie keinen Wert (oder void) zurück.

  • Kann nicht als const, oder volatilestatic. 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 rein 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.

  • Ein objekt, das mithilfe des new Operators zugewiesen wird, wird explizit mit der Verwendung deletebehandelt.

  • 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 für die Verwendung von Destruktoren:

  • Sie können ihre Adresse nicht übernehmen.

  • 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 in der Konstruktion dieser Mitglieder verwendete optionale Member-Initialisierungsliste wirkt sich nicht auf die Reihenfolge des Baus oder der Zerstörung aus.

  3. Destruktoren für nicht virtuelle Basisklassen werden in der umgekehrten 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;
}

Output: 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.
Vererbungsdiagramm mit mit virtuellen Basisklassen

Im Folgenden werden die Klassenköpfe für die in der Abbildung dargestellten 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. Handelt es sich nicht um eine Basisklasse, können Sie dies ignorieren.

  5. Sofern der gemerkte Knoten noch nicht in der Liste vorhanden ist, fügen Sie ihn unten 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. Sobald die Liste erstellt ist, wird sie in umgekehrter Reihenfolge durchlaufen, und der Destruktor wird für jede Klasse in der Liste (von der letzten bis zu ersten) aufgerufen.

In erster Linie ist die Reihenfolge zum Erstellen oder Löschen wichtig, wenn Konstruktoren oder Destruktoren einer Klasse darauf basieren, dass die andere Komponente erstellt wird oder länger vorhanden ist – beispielsweise wenn der Destruktor für A (in der obigen Abbildung) darauf basiert, dass B bei der Codeausführung noch vorhanden ist (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 der umgekehrten 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 zugewiesen, der ein Platzierungsargument verwendet. Der Operator kann diesen Speicher nicht behandeln, da es nicht aus dem kostenlosen Speicher zugewiesen wird (weitere Informationen finden Sie unter "Die deleteneuen und löschen"-Operatoren). 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 er eine Ressource erhält und die Ressource sicher verwalten muss, muss es wahrscheinlich einen Kopierkonstruktor und eine Kopiezuordnung implementieren.

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

Im nächsten Beispiel macht der implizit generierte Kopierkonstruktor die Zeiger str1.text und verweist auf denselben Speicher, und str2.text wenn wir aus copy_strings()zurückkehren, 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

Explizite Definition eines Konstruktors, eines Kopierkonstruktors oder eines Kopierzuweisungsoperators verhindert implizite Definition des Konstruktors und des Zuweisungsoperators. In diesem Fall ist es in der Regel nicht erforderlich, dass Verschiebenvorgänge nicht bereitgestellt werden, wenn das Kopieren teuer ist, eine verpasste Optimierungsmöglichkeit.

Siehe auch

Kopieren von Konstruktoren und Kopieren von Zuordnungsoperatoren
Verschieben von Konstruktoren und Verschieben von Zuordnungsoperatoren