Was ein Destruktor ist
Auf der vorherigen Seite hast du Konstruktoren gesehen - spezielle Funktionen, die laufen, wenn ein Objekt geboren wird, um seinen Anfangszustand einzurichten. Ein Destruktor ist das Spiegelbild: eine spezielle Funktion, die läuft, wenn ein Objekt stirbt, um danach aufzuräumen.
Du deklarierst ihn mit dem Klassennamen, dem eine Tilde (~) vorangestellt wird. Er nimmt keine Parameter, gibt nichts zurück, und eine Klasse kann genau einen davon haben. Du rufst ihn fast nie von Hand auf - C++ ruft ihn im richtigen Moment für dich auf.
Beachte, dass die Destruktor-Meldung nachdem main den Funktionsrumpf beendet hat, aber bevor das Programm endet, ausgegeben wird. Wenn log an der schließenden geschweiften Klammer seinen Gültigkeitsbereich verlässt, führt C++ ~Logger() für dich aus.
Wann Destruktoren laufen
Der genaue Zeitpunkt hängt davon ab, wo das Objekt lebt:
- Stack-Objekte (lokale) werden zerstört, wenn sie ihren Gültigkeitsbereich verlassen - an der schließenden
}des Blocks. - Heap-Objekte (mit
newerzeugt) werden zerstört, wenn dudeleteaufrufst. Vergisst du dasdelete, läuft der Destruktor nie und du verursachst ein Leck.
Dieses Beispiel macht den Unterschied sichtbar:
Objekte werden in umgekehrter Reihenfolge ihrer Konstruktion zerstört. a wurde zuerst gebaut, also stirbt es zuletzt. Diese LIFO-Reihenfolge (last-in, first-out - zuletzt rein, zuerst raus) ist wichtig, wenn Objekte voneinander abhängen.
Warum Destruktoren wichtig sind: RAII
Die wahre Stärke von Destruktoren liegt darin, dass sie das Aufräumen automatisch und ausnahmesicher machen. Statt daran zu denken, eine Ressource auf jedem Codepfad freizugeben, legst du die Freigabe in einen Destruktor und lässt die Sprache garantieren, dass er läuft. Dieses Muster heißt RAII - Resource Acquisition Is Initialization (Ressourcenbelegung ist Initialisierung) - und ist das Rückgrat von modernem C++.
Hier besitzt eine Klasse einen Heap-Puffer: Sie reserviert ihn im Konstruktor und gibt ihn im Destruktor frei, sodass Aufrufer new/delete nie selbst anfassen.
Die zentrale Erkenntnis: Selbst wenn nach der Erzeugung von squares eine Ausnahme geworfen würde, würde der Stack abgewickelt und ~IntArray() liefe trotzdem. Diese Garantie macht RAII so zuverlässig - und ist der Grund, warum du in gutem C++-Code selten ein nacktes delete schreibst.
Die Dreierregel (und die Fünferregel)
Eine Klasse mit eigenem Destruktor besitzt fast immer eine rohe Ressource, und das schafft eine versteckte Gefahr. Der vom Compiler erzeugte Kopierkonstruktor und die Kopierzuweisung machen eine flache Kopie - sie kopieren den Zeiger, nicht den Puffer, auf den er zeigt. Nun halten zwei Objekte denselben Zeiger, und beide Destruktoren werden ihn mit delete löschen, was einen Double-Free-Absturz verursacht.
IntArray a(5);
IntArray b = a; // flache Kopie: a.data und b.data sind DERSELBE Zeiger
// am Ende des Gültigkeitsbereichs: der Destruktor von b gibt den Puffer frei,
// dann gibt der Destruktor von a ihn ERNEUT frei -> undefiniertes Verhalten (Double Free)
Das führt zur Dreierregel: Wenn du eines der drei - Destruktor, Kopierkonstruktor oder Kopierzuweisungsoperator - schreibst, brauchst du mit ziemlicher Sicherheit alle drei. In C++11 und später erweitert sie sich zur Fünferregel, die den Move-Konstruktor und die Move-Zuweisung hinzufügt.
Es gibt jedoch eine noch bessere Regel - die Nullerregel: Entwirf Klassen so, dass du gar keine rohen Ressourcen verwaltest. Halte stattdessen einen std::vector, einen std::string oder einen Smart Pointer, und der vom Compiler erzeugte Destruktor macht das Richtige umsonst.
Greife standardmäßig zur Nullerregel. Schreibe einen eigenen Destruktor nur, wenn du wirklich eine rohe Ressource besitzt, die kein Standardtyp für dich kapselt.
Virtuelle Destruktoren
Wenn du ein Objekt über einen Zeiger auf die Basisklasse löschst, muss der Destruktor virtual sein - sonst wird nur der Basisteil zerstört und der abgeleitete Teil leckt. Das ist einer der häufigsten Fehler in polymorphem Code, und der Compiler warnt dich standardmäßig nicht davor.
Ohne virtual an ~Base würde delete p nur ~Base() aufrufen - undefiniertes Verhalten, und der Derived-Teil des Objekts wird nie aufgeräumt. Faustregel: Jede Klasse mit virtuellen Funktionen (eine polymorphe Basisklasse) braucht einen virtuellen Destruktor. Du wirst genau sehen, warum das wichtig ist, sobald du anfängst, Klassen abzuleiten.
Häufige Fehler und Fallstricke
Ein paar Fallen bringen fast jeden zu Fall:
Nicht zusammenpassende new/delete. Wenn du mit new[] reservierst, gib mit delete[] frei. new[] mit einem schlichten delete (oder umgekehrt) zu mischen, ist undefiniertes Verhalten.
virtual an einem Basis-Destruktor vergessen. Wie oben: Ein abgeleitetes Objekt über einen Basiszeiger ohne virtuellen Destruktor zu löschen, leckt den abgeleiteten Teil. Wenn du eine Klasse schreibst, von der geerbt werden soll, mach den Destruktor virtuell.
Ausnahmen aus einem Destruktor entkommen lassen. Ein Destruktor, der während der Stack-Abwicklung eine Ausnahme wirft, beendet dein Programm. In modernem C++ sind Destruktoren implizit noexcept - halte den Aufräumcode davon ab, Ausnahmen zu werfen, oder schlucke die Ausnahme innerhalb des Destruktors.
Einen Destruktor schreiben, den du nicht brauchst. Wenn deine Member sich bereits selbst aufräumen, fügt ein leerer ~Klassenname() {} nur Lärm hinzu und kann Move-Operationen stillschweigend deaktivieren. Wenn es nichts aufzuräumen gibt, schreibe gar keinen Destruktor.
Als Nächstes: Vererbung
Du hast nun den vollständigen Lebenszyklus eines Objekts gesehen - Konstruktoren erwecken es zum Leben, Destruktoren räumen es auf, und virtual-Destruktoren halten dieses Aufräumen korrekt, wenn eine Klasse auf einer anderen aufbaut. Dieser letzte Punkt ist eine Vorschau auf die nächste große Idee: Vererbung, bei der eine Klasse die Daten und das Verhalten einer anderen wiederverwendet und erweitert. Die nächste Seite zeigt, wie man eine Klasse von einer anderen ableitet, wie sich Konstruktion und Zerstörung durch die Hierarchie ketten, und wie die Teile, die du gerade gelernt hast, zusammenpassen.
Häufig gestellte Fragen
Was ist ein Destruktor in C++?
Ein Destruktor ist eine spezielle Member-Funktion namens ~Klassenname(), die automatisch läuft, wenn ein Objekt zerstört wird - wenn es seinen Gültigkeitsbereich verlässt oder du es mit delete löschst. Seine Aufgabe ist das Aufräumen: Speicher freigeben, Dateien schließen oder jede Ressource freigeben, die das Objekt besitzt. Er nimmt keine Parameter, hat keinen Rückgabetyp, und eine Klasse kann nur einen davon haben.
Wann läuft ein Destruktor in C++?
Bei einem lokalen Objekt (auf dem Stack) läuft der Destruktor, wenn es den Gültigkeitsbereich verlässt, an der schließenden }. Bei einem Heap-Objekt, das mit new erzeugt wurde, läuft er, wenn du delete aufrufst. Member und Basisklassen werden danach automatisch zerstört, in umgekehrter Reihenfolge der Konstruktion.
Muss ich in C++ immer einen Destruktor schreiben?
Nein. Wenn deine Klasse nur Member enthält, die sich selbst aufräumen (wie std::string, std::vector oder Smart Pointer), reicht der vom Compiler erzeugte Destruktor - schreibe keinen. Einen eigenen Destruktor brauchst du nur, wenn deine Klasse eine rohe Ressource besitzt, etwa Speicher aus new oder ein offenes Datei-Handle.