Hinweis
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, sich anzumelden oder das Verzeichnis zu wechseln.
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, das Verzeichnis zu wechseln.
Ein Destruktor ist eine Memberfunktion, die automatisch aufgerufen wird, wenn das Objekt außerhalb des Geltungsbereichs gerät oder durch einen Aufruf von delete
oder delete[]
explizit zerstört wird. 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 Ressourcen verwaltet, 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 freizugeben.
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. - Sie können nicht als
const
,volatile
, oderstatic
deklariert werden. Sie können jedoch für die Destruktion von Objekten aufgerufen werden, die alsconst
,volatile
, oderstatic
deklariert sind. - Kann als
virtual
deklariert 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.
- Verwenden Sie
delete
, um ein mitnew
zugewiesenes Objekt freizugeben. Die Verwendung vondelete[]
führt zu undefiniertem Verhalten. - Verwenden Sie
delete[]
, um die Zuordnung für ein Objekt aufzuheben, das unter Verwendung vonnew[]
zugeordnet wurde. Die Verwendung vondelete
führt zu undefiniertem Verhalten. - 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 seine Adresse nicht entgegennehmen.
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:
Der Destruktor der Klasse wird aufgerufen, und der Text der Destruktorfunktion wird ausgeführt.
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.
Destruktoren für nicht virtuelle Basisklassen werden in umgekehrter Reihenfolge der Deklaration aufgerufen.
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.
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:
- Durchlaufen Sie das Diagramm von links, und beginnen Sie mit dem tiefsten Punkt im Diagramm (in diesem Fall
E
). - Führen Sie Traversierungen nach links durch, bis alle Knoten besucht wurden. Notieren Sie den Namen des aktuellen Knotens.
- 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.
- 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.
- Wenn sich der gespeicherte Knoten noch nicht in der Liste befindet, fügen Sie ihn am Ende der Liste hinzu.
- Durchlaufen Sie das Diagramm nach oben und entlang des nächsten Pfads nach rechts.
- Fahren Sie mit Schritt 2 fort.
- Wenn der letzte aufwärts führende Pfad erschöpft ist, notieren Sie den Namen des aktuellen Knotens.
- Fahren Sie mit Schritt 3 fort.
- 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:
- Die nicht virtuelle Basisklasse
E
. - Die nicht virtuelle Basisklasse
D
. - Die nicht virtuelle Basisklasse
C
. - Die virtuelle Basisklasse
B
. - 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.
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 bestehen bleibt, z. B. wenn der Destruktor für A
in der zuvor gezeigten Abbildung darauf angewiesen ist, dass B
weiterhin vorhanden ist, wenn sein 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 freigeben, da er nicht aus dem freien Speicher zugeordnet ist (weitere Informationen finden Sie unter Die new- und delete-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. Dank dieser Möglichkeit können Sie explizite Aufrufe durchführen, ohne wissen zu müssen, ob für den Typ ein Destruktor definiert ist. Ein expliziter Aufruf eines Destruktors, wenn keiner definiert ist, hat keine Wirkung.
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
und str2.text
veranlassen, auf denselben Speicher zu verweisen, und wenn wir von copy_strings()
zurückkehren, wird dieser Speicher zweimal gelöscht, was zu undefiniertem Verhalten führt.
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 Nichtbereitstellen von Verschiebungsvorgängen, wenn das Kopieren in der Regel teuer ist, meist eine verpasste Optimierungschance.
Siehe auch
Kopierkonstruktoren und Kopierzuweisungsoperatoren
Bewegungskonstruktoren und Bewegungszuweisungsoperatoren