Wann wird ein C ++ - Destruktor aufgerufen?

117

Grundlegende Frage: Wann ruft ein Programm die Destruktormethode einer Klasse in C ++ auf? Mir wurde gesagt, dass es immer dann aufgerufen wird, wenn ein Objekt den Gültigkeitsbereich verlässt oder einem Objekt ausgesetzt istdelete

Spezifischere Fragen:

1) Wenn das Objekt über einen Zeiger erstellt wird und dieser Zeiger später gelöscht wird oder eine neue Adresse zum Zeigen erhält, ruft das Objekt, auf das es zeigte, seinen Destruktor auf (vorausgesetzt, nichts anderes zeigt darauf)?

2) Im Anschluss an Frage 1, was definiert, wann ein Objekt den Gültigkeitsbereich verlässt (nicht in Bezug darauf, wann ein Objekt einen bestimmten {Block} verlässt). Mit anderen Worten, wann wird ein Destruktor für ein Objekt in einer verknüpften Liste aufgerufen?

3) Möchten Sie jemals einen Destruktor manuell aufrufen?

Pat Murray
quelle
3
Auch Ihre spezifischen Fragen sind zu weit gefasst. "Dieser Zeiger wird später gelöscht" und "eine neue Adresse angegeben, auf die gezeigt werden soll" sind ganz anders. Suchen Sie mehr (einige davon wurden beantwortet) und stellen Sie dann separate Fragen für die Teile, die Sie nicht finden konnten.
Matthew Flaschen

Antworten:

73

1) Wenn das Objekt über einen Zeiger erstellt wird und dieser Zeiger später gelöscht wird oder eine neue Adresse zum Zeigen erhält, ruft das Objekt, auf das es zeigte, seinen Destruktor auf (vorausgesetzt, nichts anderes zeigt darauf)?

Dies hängt von der Art der Zeiger ab. Beispielsweise löschen intelligente Zeiger ihre Objekte häufig, wenn sie gelöscht werden. Gewöhnliche Zeiger nicht. Das Gleiche gilt, wenn ein Zeiger auf ein anderes Objekt zeigt. Einige intelligente Zeiger zerstören das alte Objekt oder zerstören es, wenn es keine Referenzen mehr hat. Gewöhnliche Zeiger haben keine solchen Intelligenz. Sie enthalten lediglich eine Adresse und ermöglichen es Ihnen, Operationen an den Objekten auszuführen, auf die sie zeigen, indem Sie dies gezielt tun.

2) Im Anschluss an Frage 1, was definiert, wann ein Objekt den Gültigkeitsbereich verlässt (nicht in Bezug darauf, wann ein Objekt einen bestimmten {Block} verlässt). Mit anderen Worten, wann wird ein Destruktor für ein Objekt in einer verknüpften Liste aufgerufen?

Das hängt von der Implementierung der verknüpften Liste ab. Typische Sammlungen zerstören alle enthaltenen Objekte, wenn sie zerstört werden.

Eine verknüpfte Liste von Zeigern würde also normalerweise die Zeiger zerstören, aber nicht die Objekte, auf die sie zeigen. (Was möglicherweise richtig ist. Dies können Verweise durch andere Zeiger sein.) Eine verknüpfte Liste, die speziell für Zeiger entwickelt wurde, kann jedoch die Objekte bei ihrer eigenen Zerstörung löschen.

Eine verknüpfte Liste von intelligenten Zeigern kann die Objekte automatisch löschen, wenn die Zeiger gelöscht werden, oder dies, wenn sie keine Referenzen mehr haben. Es liegt ganz bei Ihnen, die Teile auszuwählen, die das tun, was Sie wollen.

3) Möchten Sie jemals einen Destruktor manuell aufrufen?

Sicher. Ein Beispiel wäre, wenn Sie ein Objekt durch ein anderes Objekt desselben Typs ersetzen möchten, aber keinen Speicher freigeben möchten, nur um es erneut zuzuweisen. Sie können das alte Objekt an Ort und Stelle zerstören und ein neues an Ort und Stelle erstellen. (Im Allgemeinen ist dies jedoch eine schlechte Idee.)

// pointer is destroyed because it goes out of scope,
// but not the object it pointed to. memory leak
if (1) {
 Foo *myfoo = new Foo("foo");
}


// pointer is destroyed because it goes out of scope,
// object it points to is deleted. no memory leak
if(1) {
 Foo *myfoo = new Foo("foo");
 delete myfoo;
}

// no memory leak, object goes out of scope
if(1) {
 Foo myfoo("foo");
}
David Schwartz
quelle
2
Ich dachte, das letzte Ihrer Beispiele hat eine Funktion deklariert? Es ist ein Beispiel für die "ärgerlichste Analyse". (Der andere trivialere Punkt ist, dass Sie new Foo()mit einem Großbuchstaben 'F' gemeint haben .)
Stuart Golodetz
1
Ich denke, es Foo myfoo("foo")ist nicht Most Vexing Parse, aber es char * foo = "foo"; Foo myfoo(foo);ist.
Cosine
Es mag eine dumme Frage sein, aber sollte die nicht delete myFoovorher angerufen werden Foo *myFoo = new Foo("foo");? Oder würden Sie das neu erstellte Objekt löschen, nein?
Matheus Rocha
Es gibt keine myFoovor der Foo *myFoo = new Foo("foo");Linie. Diese Zeile erstellt eine brandneue Variable namens myFoo, die eine vorhandene Variable schattiert. In diesem Fall gibt es jedoch keine, da das myFoooben Gesagte im Geltungsbereich der ifendet.
David Schwartz
1
@galactikuh Ein "intelligenter Zeiger" verhält sich wie ein Zeiger auf ein Objekt, verfügt jedoch auch über Funktionen, die die Verwaltung der Lebensdauer dieses Objekts erleichtern.
David Schwartz
19

Andere haben die anderen Probleme bereits angesprochen, daher werde ich nur einen Punkt betrachten: Möchten Sie jemals ein Objekt manuell löschen?

Die Antwort ist ja. @ DavidSchwartz gab ein Beispiel, aber es ist ziemlich ungewöhnlich. Ich werde ein Beispiel geben, das unter der Haube dessen steht, was viele C ++ - Programmierer ständig verwenden: std::vector(und std::dequeobwohl es nicht ganz so häufig verwendet wird).

Wie die meisten Leute wissen, std::vectorwird ein größerer Speicherblock zugewiesen, wenn Sie mehr Elemente hinzufügen, als die aktuelle Zuordnung aufnehmen kann. In diesem Fall verfügt es jedoch über einen Speicherblock, der mehr Objekte aufnehmen kann, als sich derzeit im Vektor befinden.

Um dies zu verwalten, vectorwird unter dem Deckmantel Rohspeicher über das AllocatorObjekt zugewiesen (was, sofern Sie nichts anderes angeben, bedeutet, dass es verwendet wird ::operator new). Wenn Sie dann (zum Beispiel) push_backein Element zum hinzufügen vector, verwendet der Vektor intern a placement new, um ein Element im (zuvor) nicht verwendeten Teil seines Speicherplatzes zu erstellen.

Was passiert nun, wenn Sie eraseeinen Artikel aus dem Vektor haben? Es kann nicht einfach verwendet werden delete- das würde seinen gesamten Speicherblock freigeben; Es muss ein Objekt in diesem Speicher zerstören, ohne andere zu zerstören oder einen der von ihm gesteuerten Speicherblöcke freizugeben (wenn Sie beispielsweise erase5 Elemente aus einem Vektor und sofort push_back5 weitere Elemente auswählen, wird garantiert, dass der Vektor nicht neu zugewiesen wird Erinnerung, wenn Sie dies tun.

Zu diesem Zweck zerstört der Vektor die Objekte im Speicher direkt, indem er den Destruktor explizit aufruft und nicht verwendet delete.

Wenn vielleicht jemand anderes einen Container unter Verwendung eines zusammenhängenden Speichers ungefähr so vectorschreibt wie ein Do (oder eine Variante davon, wie es std::dequewirklich der Fall ist), möchten Sie mit ziemlicher Sicherheit dieselbe Technik verwenden.

Betrachten wir zum Beispiel, wie Sie Code für einen kreisförmigen Ringpuffer schreiben können.

#ifndef CBUFFER_H_INC
#define CBUFFER_H_INC

template <class T>
class circular_buffer {
    T *data;
    unsigned read_pos;
    unsigned write_pos;
    unsigned in_use;
    const unsigned capacity;
public:
    circular_buffer(unsigned size) :
        data((T *)operator new(size * sizeof(T))),
        read_pos(0),
        write_pos(0),
        in_use(0),
        capacity(size)
    {}

    void push(T const &t) {
        // ensure there's room in buffer:
        if (in_use == capacity) 
            pop();

        // construct copy of object in-place into buffer
        new(&data[write_pos++]) T(t);
        // keep pointer in bounds.
        write_pos %= capacity;
        ++in_use;
    }

    // return oldest object in queue:
    T front() {
        return data[read_pos];
    }

    // remove oldest object from queue:
    void pop() { 
        // destroy the object:
        data[read_pos++].~T();

        // keep pointer in bounds.
        read_pos %= capacity;
        --in_use;
    }

    // release the buffer:
~circular_buffer() { operator delete(data); }
};

#endif

Im Gegensatz zu den Standard - Containern dieser Anwendungen operator newund operator deletedirekt. Für den realen Gebrauch möchten Sie wahrscheinlich eine Allokatorklasse verwenden, aber im Moment würde sie mehr ablenken als beitragen (IMO jedenfalls).

Jerry Sarg
quelle
8
  1. Wenn Sie ein Objekt mit erstellen new, sind Sie für den Aufruf verantwortlich delete. Wenn Sie ein Objekt mit erstellen make_shared, ist das Ergebnis shared_ptrdafür verantwortlich, die Anzahl zu halten und aufzurufen, deletewenn die Verwendungsanzahl auf Null geht.
  2. Das Verlassen des Geltungsbereichs bedeutet, einen Block zu verlassen. In diesem Fall wird der Destruktor aufgerufen, vorausgesetzt, das Objekt wurde nicht zugewiesen new(dh es handelt sich um ein Stapelobjekt).
  3. Das einzige Mal, wenn Sie einen Destruktor explizit aufrufen müssen, ist, wenn Sie dem Objekt eine Platzierungnew zuweisen .
dasblinkenlight
quelle
1
Es gibt eine Referenzzählung (shared_ptr), obwohl dies offensichtlich nicht für einfache Zeiger gilt.
Pubby
1
@Pubby: Guter Punkt, lasst uns gute Praktiken fördern. Antwort bearbeitet.
MSalters
5

1) Objekte werden nicht 'über Zeiger' erstellt. Es gibt einen Zeiger, der jedem Objekt zugewiesen wird, das Sie "neu" sind. Angenommen, dies ist das, was Sie meinen. Wenn Sie für den Zeiger "Löschen" aufrufen, wird das Objekt, auf das sich der Zeiger bezieht, tatsächlich gelöscht (und der Destruktor aufgerufen). Wenn Sie den Zeiger einem anderen Objekt zuweisen, tritt ein Speicherverlust auf. Nichts in C ++ wird Ihren Müll für Sie sammeln.

2) Dies sind zwei getrennte Fragen. Eine Variable verlässt den Gültigkeitsbereich, wenn der Stapelrahmen, in dem sie deklariert ist, vom Stapel entfernt wird. Normalerweise verlassen Sie einen Block. Objekte in einem Heap verlassen nie den Gültigkeitsbereich, obwohl ihre Zeiger auf dem Stapel dies möglicherweise tun. Nichts garantiert, dass ein Destruktor eines Objekts in einer verknüpften Liste aufgerufen wird.

3) Nicht wirklich. Möglicherweise gibt es Deep Magic, das etwas anderes vorschlägt, aber normalerweise möchten Sie Ihre "neuen" Schlüsselwörter mit Ihren "Lösch" -Schlüsselwörtern abgleichen und alles in Ihren Destruktor einfügen, was erforderlich ist, um sicherzustellen, dass es sich ordnungsgemäß bereinigt. Wenn Sie dies nicht tun, kommentieren Sie den Destruktor mit spezifischen Anweisungen an alle Benutzer der Klasse, wie sie die Ressourcen dieses Objekts manuell bereinigen sollen.

Nathaniel Ford
quelle
3

Um eine detaillierte Antwort auf Frage 3 zu geben: Ja, es gibt (seltene) Fälle, in denen Sie den Destruktor explizit aufrufen könnten, insbesondere als Gegenstück zu einer neuen Platzierung, wie dasblinkenlight feststellt.

Um ein konkretes Beispiel dafür zu geben:

#include <iostream>
#include <new>

struct Foo
{
    Foo(int i_) : i(i_) {}
    int i;
};

int main()
{
    // Allocate a chunk of memory large enough to hold 5 Foo objects.
    int n = 5;
    char *chunk = static_cast<char*>(::operator new(sizeof(Foo) * n));

    // Use placement new to construct Foo instances at the right places in the chunk.
    for(int i=0; i<n; ++i)
    {
        new (chunk + i*sizeof(Foo)) Foo(i);
    }

    // Output the contents of each Foo instance and use an explicit destructor call to destroy it.
    for(int i=0; i<n; ++i)
    {
        Foo *foo = reinterpret_cast<Foo*>(chunk + i*sizeof(Foo));
        std::cout << foo->i << '\n';
        foo->~Foo();
    }

    // Deallocate the original chunk of memory.
    ::operator delete(chunk);

    return 0;
}

Der Zweck dieser Art von Dingen besteht darin, die Speicherzuordnung von der Objektkonstruktion zu entkoppeln.

Stuart Golodetz
quelle
2
  1. Zeiger - Normale Zeiger unterstützen RAII nicht. Ohne eine explizite deletewird es Müll geben. Glücklicherweise hat C ++ automatische Zeiger , die dies für Sie erledigen!

  2. Bereich - Überlegen Sie, wann eine Variable für Ihr Programm unsichtbar wird . Normalerweise ist dies am Ende von {block}, wie Sie betonen.

  3. Manuelle Zerstörung - Versuchen Sie dies niemals. Lassen Sie einfach Scope und RAII die Magie für Sie tun.

Chrisaycock
quelle
Ein Hinweis: auto_ptr ist veraltet, wie in Ihrem Link erwähnt.
tnecniv
std::auto_ptrist in C ++ 11 veraltet, ja. Wenn das OP tatsächlich über C ++ 11 verfügt, sollte es std::unique_ptrfür einzelne Eigentümer oder std::shared_ptrfür mehrere Eigentümer mit Referenzzählung verwendet werden.
Chrisaycock
"Manuelle Zerstörung - Versuchen Sie dies niemals". Ich stelle Objektzeiger sehr oft mit einem Systemaufruf, den der Compiler nicht versteht, in eine Warteschlange für einen anderen Thread. Das "Verlassen" auf Scope / Auto / Smart-Zeiger würde dazu führen, dass meine Apps katastrophal fehlschlagen, da Objekte vom aufrufenden Thread gelöscht wurden, bevor sie vom Consumer-Thread verarbeitet werden konnten. Dieses Problem betrifft Objekte und Schnittstellen mit eingeschränktem Umfang und refCounted. Nur Zeiger und explizites Löschen reichen aus.
Martin James
@MartinJames Können Sie ein Beispiel für einen Systemaufruf veröffentlichen, den der Compiler nicht versteht? Und wie implementieren Sie die Warteschlange? Nicht std::queue<std::shared_ptr>?habe ich festgestellt , dass pipe()zwischen einem Erzeuger und Verbraucher Thread viel einfacher Parallelität, wenn das Kopieren nicht zu teuer ist.
Chrisaycock
myObject = new myClass (); PostMessage (aHandle, WM_APP, 0, LPPARAM (myObject));
Martin James
1

Immer wenn Sie "neu" verwenden, dh eine Adresse an einen Zeiger anhängen oder sagen, Sie beanspruchen Speicherplatz auf dem Heap, müssen Sie ihn "löschen".
1. Ja, wenn Sie etwas löschen, wird der Destruktor aufgerufen.
2.Wenn der Destruktor der verknüpften Liste aufgerufen wird, wird der Destruktor der Objekte aufgerufen. Wenn es sich jedoch um Zeiger handelt, müssen Sie sie manuell löschen. 3.wenn der Platz von "neu" beansprucht wird.

bewölkt
quelle
0

Ja, ein Destruktor (auch bekannt als dtor) wird aufgerufen, wenn ein Objekt den Gültigkeitsbereich verlässt, wenn es sich auf dem Stapel befindet oder wenn Sie deleteeinen Zeiger auf ein Objekt aufrufen .

  1. Wenn der Zeiger über gelöscht wird, deletewird der dtor aufgerufen. Wenn Sie den Zeiger neu zuweisen, ohne ihn deletevorher aufzurufen , tritt ein Speicherverlust auf, da das Objekt noch irgendwo im Speicher vorhanden ist. Im letzteren Fall wird der dtor nicht aufgerufen.

  2. Eine gute Implementierung einer verknüpften Liste ruft den dtor aller Objekte in der Liste auf, wenn die Liste zerstört wird (weil Sie entweder eine Methode aufgerufen haben, um sie zu zerstören, oder sie selbst den Gültigkeitsbereich verlassen hat). Dies ist implementierungsabhängig.

  3. Ich bezweifle es, aber ich wäre nicht überrascht, wenn es da draußen einen merkwürdigen Umstand gibt.

tnecniv
quelle
1
"Wenn Sie den Zeiger neu zuweisen, ohne zuerst delete aufzurufen, tritt ein Speicherverlust auf, da das Objekt noch irgendwo im Speicher vorhanden ist." Nicht unbedingt. Es könnte durch einen anderen Zeiger gelöscht worden sein.
Matthew Flaschen
0

Wenn das Objekt nicht über einen Zeiger erstellt wird (z. B. A a1 = A ();), wird der Destruktor aufgerufen, wenn das Objekt zerstört wird, immer dann, wenn die Funktion, in der das Objekt liegt, beendet ist. Beispiel:

void func()
{
...
A a1 = A();
...
}//finish


Der Destruktor wird aufgerufen, wenn Code ausgeführt wird, um "finish" zu setzen.

Wenn das Objekt über einen Zeiger erstellt wird (z. B. A * a2 = new A ();), wird der Destruktor aufgerufen, wenn der Zeiger gelöscht wird (delete a2;). Wenn der Punkt vom Benutzer nicht explizit gelöscht oder mit a versehen wird Neue Adresse vor dem Löschen, der Speicherverlust ist aufgetreten. Das ist ein Fehler.

Wenn wir in einer verknüpften Liste std :: list <> verwenden, müssen wir uns nicht um den Desktruktor oder den Speicherverlust kümmern, da std :: list <> all dies für uns erledigt hat. In einer von uns selbst geschriebenen verknüpften Liste sollten wir den Desktruktor schreiben und den Zeiger explizit löschen. Andernfalls führt dies zu einem Speicherverlust.

Wir rufen selten einen Destruktor manuell auf. Es ist eine Funktion, die das System bereitstellt.

Tut mir leid für mein schlechtes Englisch!

Wyx
quelle
Es ist nicht wahr, dass Sie einen Destruktor nicht manuell aufrufen können - Sie können (siehe zum Beispiel den Code in meiner Antwort). Was wahr ist, ist, dass die überwiegende Mehrheit der Zeit Sie nicht sollten :)
Stuart Golodetz
0

Denken Sie daran, dass der Konstruktor eines Objekts unmittelbar nach der Zuweisung des Speichers für dieses Objekt aufgerufen wird und der Destruktor unmittelbar vor der Freigabe des Speichers dieses Objekts aufgerufen wird.

Sunny Khandare
quelle