Wann genau werden Objekte in C ++ zerstört und was bedeutet das? Muss ich sie manuell zerstören, da es keinen Garbage Collector gibt? Wie kommen Ausnahmen ins Spiel?
(Hinweis: Dies ist als Eintrag in die C ++ - FAQ von Stack Overflow gedacht . Wenn Sie die Idee kritisieren möchten, eine FAQ in dieser Form bereitzustellen, ist die Veröffentlichung auf Meta, mit der all dies begonnen hat , der richtige Ort dafür. Antworten auf Diese Frage wird im C ++ - Chatroom überwacht, in dem die FAQ-Idee ursprünglich begann. Daher wird Ihre Antwort sehr wahrscheinlich von denjenigen gelesen, die auf die Idee gekommen sind.)
c++
exception
destructor
c++-faq
object-lifetime
Fredoverflow
quelle
quelle
Antworten:
Im folgenden Text werde ich zwischen Objekten mit Gültigkeitsbereich unterscheiden , deren Zerstörungszeitpunkt statisch durch ihren umschließenden Bereich (Funktionen, Blöcke, Klassen, Ausdrücke) bestimmt wird, und dynamischen Objekten , deren genaue Zerstörungszeit zum Zeitpunkt der Laufzeit im Allgemeinen nicht bekannt ist.
Während die Zerstörungssemantik von Klassenobjekten durch Destruktoren bestimmt wird, ist die Zerstörung eines skalaren Objekts immer ein No-Op. Insbesondere wird eine Zeigervariable zerstörenden nicht zerstören die pointee.
Objekte mit Gültigkeitsbereich
automatische Objekte
Automatische Objekte (üblicherweise als "lokale Variablen" bezeichnet) werden in umgekehrter Reihenfolge ihrer Definition zerstört, wenn der Kontrollfluss den Umfang ihrer Definition verlässt:
void some_function() { Foo a; Foo b; if (some_condition) { Foo y; Foo z; } <--- z and y are destructed here } <--- b and a are destructed here
Wenn während der Ausführung einer Funktion eine Ausnahme ausgelöst wird, werden alle zuvor erstellten automatischen Objekte zerstört, bevor die Ausnahme an den Aufrufer weitergegeben wird. Dieser Vorgang wird als Abwickeln des Stapels bezeichnet . Während des Abwickelns des Stapels dürfen keine weiteren Ausnahmen die Destruktoren der zuvor erwähnten zuvor konstruierten automatischen Objekte verlassen. Andernfalls wird die Funktion
std::terminate
aufgerufen.Dies führt zu einer der wichtigsten Richtlinien in C ++:
nicht lokale statische Objekte
Statische Objekte, die im Namespace-Bereich definiert sind (üblicherweise als "globale Variablen" bezeichnet), und statische Datenelemente werden in umgekehrter Reihenfolge ihrer Definition nach der Ausführung von
main
:struct X { static Foo x; // this is only a *declaration*, not a *definition* }; Foo a; Foo b; int main() { } <--- y, x, b and a are destructed here Foo X::x; // this is the respective definition Foo y;
Beachten Sie, dass die relative Reihenfolge der Konstruktion (und Zerstörung) statischer Objekte, die in verschiedenen Übersetzungseinheiten definiert sind, nicht definiert ist.
Wenn eine Ausnahme den Destruktor eines statischen Objekts verlässt, wird die Funktion
std::terminate
aufgerufen.lokale statische Objekte
In Funktionen definierte statische Objekte werden erstellt, wenn (und wenn) der Steuerungsfluss zum ersten Mal ihre Definition durchläuft. 1 Sie werden in umgekehrter Reihenfolge nach der Ausführung von
main
:Foo& get_some_Foo() { static Foo x; return x; } Bar& get_some_Bar() { static Bar y; return y; } int main() { get_some_Bar().do_something(); // note that get_some_Bar is called *first* get_some_Foo().do_something(); } <--- x and y are destructed here // hence y is destructed *last*
Wenn eine Ausnahme den Destruktor eines statischen Objekts verlässt, wird die Funktion
std::terminate
aufgerufen.1: Dies ist ein extrem vereinfachtes Modell. Die Initialisierungsdetails von statischen Objekten sind tatsächlich viel komplizierter.
Unterobjekte der Basisklasse und Unterobjekte der Mitglieder
Wenn der Steuerungsfluss den Destruktorkörper eines Objekts verlässt, werden seine Elementunterobjekte (auch als "Datenelemente" bezeichnet) in umgekehrter Reihenfolge ihrer Definition zerstört. Danach werden die Unterobjekte der Basisklasse in umgekehrter Reihenfolge der Basis-Spezifizierer-Liste zerstört:
class Foo : Bar, Baz { Quux x; Quux y; public: ~Foo() { } <--- y and x are destructed here, }; followed by the Baz and Bar base class subobjects
Wenn während der Erstellung eines der
Foo
Unterobjekte eine Ausnahme ausgelöst wird , werden alle zuvor erstellten Unterobjekte zerstört, bevor die Ausnahme weitergegeben wird. DerFoo
Destruktor hingegen wird nicht ausgeführt, da dasFoo
Objekt nie vollständig erstellt wurde.Beachten Sie, dass der Destruktorkörper nicht für die Zerstörung der Datenelemente selbst verantwortlich ist. Sie müssen einen Destruktor nur schreiben, wenn ein Datenelement ein Handle für eine Ressource ist, die freigegeben werden muss, wenn das Objekt zerstört wird (z. B. eine Datei, ein Socket, eine Datenbankverbindung, ein Mutex oder ein Heapspeicher).
Array-Elemente
Array-Elemente werden in absteigender Reihenfolge zerstört. Wenn während der Konstruktion des n-ten Elements eine Ausnahme ausgelöst wird , werden die Elemente n-1 bis 0 zerstört, bevor die Ausnahme weitergegeben wird.
temporäre Objekte
Ein temporäres Objekt wird erstellt, wenn ein prvalue-Ausdruck vom Klassentyp ausgewertet wird. Das bekannteste Beispiel für einen prvalue-Ausdruck ist der Aufruf einer Funktion, die ein Objekt nach Wert zurückgibt, z
T operator+(const T&, const T&)
. Unter normalen Umständen wird das temporäre Objekt zerstört, wenn der vollständige Ausdruck, der den pr-Wert lexikalisch enthält, vollständig ausgewertet wird:__________________________ full-expression ___________ subexpression _______ subexpression some_function(a + " " + b); ^ both temporary objects are destructed here
Der obige Funktionsaufruf
some_function(a + " " + b)
ist ein vollständiger Ausdruck, da er nicht Teil eines größeren Ausdrucks ist (stattdessen ist er Teil einer Ausdrucksanweisung). Daher werden alle temporären Objekte, die während der Auswertung der Unterausdrücke erstellt werden, im Semikolon zerstört. Es gibt zwei solche temporären Objekte: Das erste wird während der ersten Addition konstruiert und das zweite wird während der zweiten Addition konstruiert. Das zweite temporäre Objekt wird vor dem ersten zerstört.Wenn während des zweiten Hinzufügens eine Ausnahme ausgelöst wird, wird das erste temporäre Objekt ordnungsgemäß zerstört, bevor die Ausnahme weitergegeben wird.
Wenn eine lokale Referenz mit einem prvalue-Ausdruck initialisiert wird, wird die Lebensdauer des temporären Objekts auf den Bereich der lokalen Referenz erweitert, sodass Sie keine baumelnde Referenz erhalten:
{ const Foo& r = a + " " + b; ^ first temporary (a + " ") is destructed here // ... } <--- second temporary (a + " " + b) is destructed not until here
Wenn ein prvalue-Ausdruck vom Typ einer Nichtklasse ausgewertet wird, ist das Ergebnis ein Wert und kein temporäres Objekt. Ein temporäres Objekt wird jedoch erstellt, wenn der Wert zum Initialisieren einer Referenz verwendet wird:
const int& r = i + j;
Dynamische Objekte und Arrays
Im folgenden Abschnitt bedeutet "X zerstören " " X zuerst zerstören und dann den zugrunde liegenden Speicher freigeben". In ähnlicher Weise bedeutet " X erstellen " "zuerst genügend Speicher zuweisen und dann dort X erstellen ".
dynamische Objekte
Ein dynamisches Objekt, das über erstellt wurde, wird über
p = new Foo
zerstörtdelete p
. Wenn Sie dies vergessen,delete p
liegt ein Ressourcenleck vor. Sie sollten niemals versuchen, eine der folgenden Aktionen auszuführen, da alle zu undefiniertem Verhalten führen:delete[]
(beachten Sie die eckigen Klammern)free
oder auf andere WeiseWenn während der Erstellung eines dynamischen Objekts eine Ausnahme ausgelöst wird , wird der zugrunde liegende Speicher freigegeben, bevor die Ausnahme weitergegeben wird. (Der Destruktor wird vor der Speicherfreigabe nicht ausgeführt, da das Objekt nie vollständig erstellt wurde.)
dynamische Arrays
Ein über erstelltes dynamisches Array wird über
p = new Foo[n]
zerstörtdelete[] p
(beachten Sie die eckigen Klammern). Wenn Sie dies vergessen,delete[] p
liegt ein Ressourcenleck vor. Sie sollten niemals versuchen, eine der folgenden Aktionen auszuführen, da alle zu undefiniertem Verhalten führen:delete
,free
oder jegliche andere MittelWenn während der Konstruktion des n-ten Elements eine Ausnahme ausgelöst wird , werden die Elemente n-1 bis 0 in absteigender Reihenfolge zerstört, der zugrunde liegende Speicher wird freigegeben und die Ausnahme wird weitergegeben.
(Sie sollen in der Regel lieber
std::vector<Foo>
überFoo*
für dynamischen Arrays. Es macht richtig und robusten Code zu schreiben viel einfacher.)Referenzzählen von intelligenten Zeigern
Ein dynamisches Objekt, das von mehreren
std::shared_ptr<Foo>
Objekten verwaltet wird , wird während der Zerstörung des letztenstd::shared_ptr<Foo>
Objekts zerstört, das an der Freigabe dieses dynamischen Objekts beteiligt ist.(Sie sollen in der Regel lieber
std::shared_ptr<Foo>
überFoo*
für gemeinsam genutzte Objekte. Es macht viel einfacher , korrekten und robusten Code zu schreiben.)quelle
std::vector<Foo>
überFoo*
für dynamischen Arrays.“ - Eigentlich ist die meiste Zeitstd::deque<Foo>
eine bessere Wahl alsstd::vector<Foo>
, aber dies ist eine andere Diskussion.std::vector
anstelle von verwendetstd::deque
. Ich spreche hier nur für mich selbst, aber ich mag es, wenn mein Gedächtnis zusammenhängend ist.resize()
bevor sie Elemente einfügen :)Der Destruktor eines Objekts wird automatisch aufgerufen, wenn die Lebensdauer des Objekts endet und es zerstört wird. Sie sollten es normalerweise nicht manuell aufrufen.
Wir werden dieses Objekt als Beispiel verwenden:
class Test { public: Test() { std::cout << "Created " << this << "\n";} ~Test() { std::cout << "Destroyed " << this << "\n";} Test(Test const& rhs) { std::cout << "Copied " << this << "\n";} Test& operator=(Test const& rhs) { std::cout << "Assigned " << this << "\n";} };
In C ++ gibt es drei (vier in C ++ 11) verschiedene Objekttypen, und der Objekttyp definiert die Lebensdauer des Objekts.
Objekte mit statischer Speicherdauer
Dies sind die einfachsten und entsprechen globalen Variablen. Die Lebensdauer dieser Objekte entspricht (normalerweise) der Länge der Anwendung. Diese werden (normalerweise) erstellt, bevor main eingegeben und zerstört wird (in umgekehrter Reihenfolge, nachdem sie erstellt wurden), nachdem wir main verlassen haben.
Test global; int main() { std::cout << "Main\n"; } > ./a.out Created 0x10fbb80b0 Main Destroyed 0x10fbb80b0
Hinweis 1: Es gibt zwei andere Arten von statischen Speicherdauerobjekten.
statische Mitgliedsvariablen einer Klasse.
Diese sind in jeder Hinsicht dieselben wie globale Variablen in Bezug auf die Lebensdauer.
statische Variablen innerhalb einer Funktion.
Hierbei handelt es sich um träge erstellte Objekte mit statischer Speicherdauer. Sie werden bei der ersten Verwendung erstellt (in einem thread-sicheren Manor für C ++ 11). Genau wie andere Objekte mit statischer Speicherdauer werden sie beim Beenden der Anwendung zerstört.
Reihenfolge der Errichtung / Zerstörung
Objekte mit automatischer Speicherdauer
Dies sind die häufigsten Objekttypen und das, was Sie in 99% der Fälle verwenden sollten.
Dies sind drei Haupttypen von automatischen Variablen:
Lokale Variablen
Wenn eine Funktion / ein Block beendet wird, werden alle in dieser Funktion / diesem Block deklarierten Variablen zerstört (in umgekehrter Reihenfolge der Erstellung).
int main() { std::cout << "Main() START\n"; Test scope1; Test scope2; std::cout << "Main Variables Created\n"; { std::cout << "\nblock 1 Entered\n"; Test blockScope; std::cout << "block 1 about to leave\n"; } // blockScope is destrpyed here { std::cout << "\nblock 2 Entered\n"; Test blockScope; std::cout << "block 2 about to leave\n"; } // blockScope is destrpyed here std::cout << "\nMain() END\n"; }// All variables from main destroyed here. > ./a.out Main() START Created 0x7fff6488d938 Created 0x7fff6488d930 Main Variables Created block 1 Entered Created 0x7fff6488d928 block 1 about to leave Destroyed 0x7fff6488d928 block 2 Entered Created 0x7fff6488d918 block 2 about to leave Destroyed 0x7fff6488d918 Main() END Destroyed 0x7fff6488d930 Destroyed 0x7fff6488d938
Mitgliedsvariablen
Die Lebensdauer einer Mitgliedsvariablen ist an das Objekt gebunden, dem sie gehört. Wenn die Lebensdauer eines Besitzers endet, endet auch die Lebensdauer aller Mitglieder. Sie müssen sich also die Lebensdauer eines Eigentümers ansehen, der die gleichen Regeln befolgt.
Hinweis: Mitglieder werden immer vor dem Eigentümer in umgekehrter Reihenfolge der Erstellung zerstört.
und in der umgekehrten Reihenfolge der Deklaration vernichtet
und in umgekehrter Reihenfolge oben -> 0 zerstört
temporäre Variablen
Dies sind Objekte, die als Ergebnis eines Ausdrucks erstellt, aber keiner Variablen zugewiesen werden. Temporäre Variablen werden wie andere automatische Variablen zerstört. Es ist nur so, dass das Ende ihres Geltungsbereichs das Ende der Aussage ist, in der sie erstellt wurden (dies ist normalerweise das ';').
std::string data("Text."); std::cout << (data + 1); // Here we create a temporary object. // Which is a std::string with '1' added to "Text." // This object is streamed to the output // Once the statement has finished it is destroyed. // So the temporary no longer exists after the ';'
Hinweis: Es gibt Situationen, in denen die Lebensdauer eines Provisoriums verlängert werden kann.
Dies ist jedoch für diese einfache Diskussion nicht relevant. Wenn Sie verstehen, dass dieses Dokument für Sie selbstverständlich ist und bevor es die Lebensdauer eines temporären Dokuments verlängert, möchten Sie dies nicht tun.
Objekte mit dynamischer Speicherdauer
Diese Objekte haben eine dynamische Lebensdauer und werden
new
mit einem Aufruf von erstellt und zerstörtdelete
.int main() { std::cout << "Main()\n"; Test* ptr = new Test(); delete ptr; std::cout << "Main Done\n"; } > ./a.out Main() Created 0x1083008e0 Destroyed 0x1083008e0 Main Done
Für Entwickler, die aus durch Müll gesammelten Sprachen stammen, kann dies seltsam erscheinen (Verwaltung der Lebensdauer Ihres Objekts). Aber das Problem ist nicht so schlimm, wie es scheint. In C ++ ist es ungewöhnlich, dynamisch zugewiesene Objekte direkt zu verwenden. Wir haben Verwaltungsobjekte, um deren Lebensdauer zu steuern.
Das, was den meisten anderen von GC gesammelten Sprachen am nächsten kommt, ist das
std::shared_ptr
. Dadurch wird die Anzahl der Benutzer eines dynamisch erstellten Objekts verfolgt unddelete
automatisch aufgerufen, wenn alle Benutzer verschwunden sind (ich halte dies für eine bessere Version eines normalen Java-Objekts).int main() { std::cout << "Main Start\n"; std::shared_ptr<Test> smartPtr(new Test()); std::cout << "Main End\n"; } // smartPtr goes out of scope here. // As there are no other copies it will automatically call delete on the object // it is holding. > ./a.out Main Start Created 0x1083008e0 Main Ended Destroyed 0x1083008e0
Thread-Speicherdauerobjekte
Diese sind neu in der Sprache. Sie sind Objekten mit statischer Speicherdauer sehr ähnlich. Aber anstatt dasselbe Leben wie die Anwendung zu führen, leben sie so lange wie der Faden der Ausführung, mit dem sie verbunden sind.
quelle