Objektzerstörung in C ++

73

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.)

Fredoverflow
quelle
5
Wer stimmte für nahe diesem Thema? Ich sehe keinen Grund. In der Tat ist es sicherlich eine gute FAQ. +1
Nawaz
3
@Nawaz: Aber ist das eine gute Frage (was sind die Kriterien für SO), wenn sie nicht von jemandem gestellt wurde, der die Antwort tatsächlich wissen musste? Wenn es so häufig gefragt wird, warum musste @Fred es selbst fragen, damit er eine Antwort geben konnte? Mein Punkt ist einfach, dass, wenn Sie sich an die Regeln halten, "es ist eine gute FAQ" keine Rolle spielt, was zählt, "ist dies eine gute Frage ", und ich beurteile dies zumindest danach, ob es wahrscheinlich ist um dem OP die Antwort zu geben, die er benötigt (was in diesem Fall nichtig ist, weil das OP die Antwort kennt) und ob andere mit demselben Problem sie wahrscheinlich finden.
Jalf
3
Ob die Clique, die FAQ-Fragen stellt und die Antworten kennt, es bereits als "gute FAQ"
ansieht,
7
"Wenn es so häufig gefragt wird, warum musste @Fred es selbst fragen" - normalerweise, weil bestimmte Leute, die nichts über dieses Zeug wissen, nicht daran denken, zu fragen, "wann Objekte zerstört werden", sondern sie fragen nach bestimmten Frage zu ihrem speziellen Code, auf den die Antwort lautet: "Sie müssen die Lebensdauer Ihrer Objekte verstehen". Die spezifischen Fragen enthalten also zu viele Details, die für andere Fragesteller zum selben Thema irrelevant sind. Ich weiß nicht, ob dies hier der Fall ist, aber es sind die FAQ-Fragen, auf die ich in der Vergangenheit Bezug genommen habe.
Steve Jessop
3
@jalf: An selbst beantworteten Fragen ist nichts auszusetzen. In der Tat ist es ermutigt.
Thomas Bonini

Antworten:

87

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::terminateaufgerufen.

Dies führt zu einer der wichtigsten Richtlinien in C ++:

Zerstörer sollten niemals werfen.

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::terminateaufgerufen.

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::terminateaufgerufen.

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 FooUnterobjekte eine Ausnahme ausgelöst wird , werden alle zuvor erstellten Unterobjekte zerstört, bevor die Ausnahme weitergegeben wird. Der FooDestruktor hingegen wird nicht ausgeführt, da das FooObjekt 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 Foozerstört delete p. Wenn Sie dies vergessen, delete pliegt ein Ressourcenleck vor. Sie sollten niemals versuchen, eine der folgenden Aktionen auszuführen, da alle zu undefiniertem Verhalten führen:

  • Zerstören Sie ein dynamisches Objekt über delete[](beachten Sie die eckigen Klammern) freeoder auf andere Weise
  • Zerstöre ein dynamisches Objekt mehrmals
  • Zugriff auf ein dynamisches Objekt, nachdem es zerstört wurde

Wenn 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ört delete[] p(beachten Sie die eckigen Klammern). Wenn Sie dies vergessen, delete[] pliegt ein Ressourcenleck vor. Sie sollten niemals versuchen, eine der folgenden Aktionen auszuführen, da alle zu undefiniertem Verhalten führen:

  • zerstört ein dynamisches Array über delete, freeoder jegliche andere Mittel
  • Zerstören Sie ein dynamisches Array mehrmals
  • Zugriff auf ein dynamisches Array, nachdem es zerstört wurde

Wenn 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>über Foo*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 letzten std::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>über Foo*für gemeinsam genutzte Objekte. Es macht viel einfacher , korrekten und robusten Code zu schreiben.)

Fredoverflow
quelle
Es gibt keine Erwähnung der Reihenfolge der Zerstörung für statische lokale Variablen gegen statische globale Variablen
Nick
Ich schlage vor, den Fall, in dem Sie ein automatisches Objekt in einer nicht leeren Funktion haben, detailliert zu beschreiben.
AerandiR
@FredOverflow In Bezug auf „Sie sollten in der Regel lieber std::vector<Foo>über Foo*für dynamischen Arrays.“ - Eigentlich ist die meiste Zeit std::deque<Foo>eine bessere Wahl als std::vector<Foo>, aber dies ist eine andere Diskussion.
Mihai Todor
@MihaiTodor Ich habe gesehen, dass ziemlich viel gepredigt wurde, aber es scheint in der Praxis, dass jeder std::vectoranstelle von verwendet std::deque. Ich spreche hier nur für mich selbst, aber ich mag es, wenn mein Gedächtnis zusammenhängend ist.
Fredoverflow
@FredOverflow Hoffentlich werden sich die Benutzer entsprechend daran erinnern, resize()bevor sie Elemente einfügen :)
Mihai Todor
36

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
  • Objekte mit automatischer Speicherdauer
  • Objekte mit dynamischer Speicherdauer
  • (In C ++ 11) Objekte mit Thread-Speicherdauer

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

  • Die Reihenfolge der Konstruktion innerhalb einer Zusammenstellungseinheit ist genau definiert und entspricht der Deklaration.
  • Die Reihenfolge der Konstruktion zwischen Kompilierungseinheiten ist undefiniert.
  • Die Reihenfolge der Zerstörung ist die genaue Umkehrung der Reihenfolge der Konstruktion.

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 innerhalb einer Funktion / eines Blocks
  • Mitgliedsvariablen innerhalb einer Klasse / eines Arrays.
  • temporäre 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.

  • Für Klassenmitglieder werden sie also in der Reihenfolge der Deklaration erstellt
    und in der umgekehrten Reihenfolge der Deklaration vernichtet
  • Daher werden Array-Mitglieder in der Reihenfolge 0 -> oben erstellt
    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 newmit einem Aufruf von erstellt und zerstört delete.

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 und deleteautomatisch 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.

Martin York
quelle