wake-up-neo.net

Wann wird ein C++ - Destruktor aufgerufen?

Grundlegende Frage: Wann ruft ein Programm die Destruktor-Methode einer Klasse in C++ auf? Mir wurde gesagt, dass es aufgerufen wird, wenn ein Objekt den Gültigkeitsbereich verlässt oder einer delete ausgesetzt ist.

Spezifischere Fragen:

1) Wenn das Objekt über einen Zeiger erstellt wird und dieser Zeiger später gelöscht wird oder eine neue Adresse erhält, auf die er verweist, kann das Objekt, auf das es gezeigt hat, seinen Destruktor aufrufen (vorausgesetzt, es ist nichts anderes darauf zu sehen)?

2) Im Anschluss an Frage 1 wird festgelegt, wann ein Objekt den Gültigkeitsbereich verlässt (nicht, wenn 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?

90
Pat Murray

1) Wenn das Objekt über einen Zeiger erstellt wird und dieser Zeiger später gelöscht wird oder eine neue Adresse erhält, auf die er verweist, kann das Objekt, auf das es gezeigt hat, seinen Destruktor aufrufen (vorausgesetzt, es ist nichts anderes darauf zu sehen)?

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

2) Im Anschluss an Frage 1 wird festgelegt, wann ein Objekt den Gültigkeitsbereich verlässt (nicht, wenn 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. Bei typischen Sammlungen werden alle darin enthaltenen Objekte zerstört, wenn sie zerstört werden.

In einer verknüpften Liste von Zeigern werden normalerweise die Zeiger zerstört, nicht jedoch die Objekte, auf die sie zeigen. (Das kann richtig sein. Sie können Verweise von anderen Zeigern sein.) Eine verknüpfte Liste, die speziell für Zeiger entworfen 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 wenn sie keine weiteren Verweise mehr haben. Es liegt an Ihnen, die Stücke auszuwählen, die 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, den Speicher jedoch nicht freigeben möchten, 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");
}
59
David Schwartz

Andere haben sich bereits mit den anderen Problemen befasst. Ich möchte nur auf einen Punkt eingehen: Möchten Sie ein Objekt jemals manuell löschen?.

Die Antwort ist ja. @DavidSchwartz gab ein Beispiel, aber es ist ein ungewöhnliches ziemlich. Ich gebe ein Beispiel, das unter der Haube dessen steht, was viele C++ - Programmierer ständig verwenden: std::vector (und std::deque, obwohl es nicht so oft verwendet wird).

Wie den meisten Leuten bekannt ist, wird std::vector einen größeren Speicherblock zuweisen, wenn/wenn Sie mehr Elemente hinzufügen, als in der aktuellen Zuordnung möglich sind. In diesem Fall verfügt es jedoch über einen Speicherblock, der mehr Objekte aufnehmen kann, als sich derzeit im Vektor befinden.

Um dies zu bewerkstelligen, muss vector unter den Deckungen den Speicher raw über das Allocator-Objekt zuordnen (was, sofern Sie nichts anderes angeben, bedeutet, dass es ::operator new verwendet). Wenn Sie dann zum Beispiel Push_back verwenden, um der vector ein Element hinzuzufügen, verwendet der Vektor intern einen placement new, um ein Element im (zuvor) nicht verwendeten Teil seines Speicherbereichs zu erstellen.

Was passiert nun, wenn/wenn Sie ein Element aus dem Vektor eraseen? Es kann nicht einfach delete verwendet werden - das würde seinen gesamten Speicherblock freigeben; es muss ein Objekt in diesem Speicher zerstören, ohne andere Objekte zu zerstören oder einen der von ihm kontrollierten Speicherblock freizugeben (wenn Sie beispielsweise erase 5 Elemente aus einem Vektor und sofort Push_back 5 weitere Elemente enthalten, ist es Guaranteed dass der Vektor not Speicher neu zuweisen wird, wenn Sie dies tun.

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

Wenn jemand anderes einen Container mit zusammenhängendem Speicher schreiben würde, etwa wie vector (oder eine andere Variante davon, wie std::deque wirklich), würden Sie höchstwahrscheinlich dieselbe Technik verwenden.

Betrachten wir beispielsweise, wie Sie Code für einen 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 Standardcontainern werden operator new und operator delete direkt verwendet. Für den realen Einsatz möchten Sie wahrscheinlich eine Allokator-Klasse verwenden, aber im Moment würde dies mehr tun, als abzulenken, als beizutragen (jedenfalls IMO).

13
Jerry Coffin
  1. Wenn Sie ein Objekt mit new erstellen, sind Sie für den Aufruf von delete verantwortlich. Wenn Sie ein Objekt mit make_shared erstellen, ist der resultierende shared_ptr dafür verantwortlich, die Zählung aufrechtzuerhalten und delete aufzurufen, wenn die Verwendungsanzahl auf null geht.
  2. Wenn Sie den Bereich verlassen, müssen Sie einen Block verlassen. Wenn der Destruktor aufgerufen wird, wird angenommen, dass das Objekt not mit new zugewiesen wurde (d. H., Es ist ein Stack-Objekt).
  3. Der einzige Zeitpunkt, zu dem Sie einen Destruktor explizit aufrufen müssen, ist, wenn Sie das Objekt mit einer Platzierung new zuordnen.
5
dasblinkenlight

Um eine detaillierte Antwort auf Frage 3 zu geben: Ja, es gibt (seltene) Fälle, in denen Sie den Destruktor explizit nennen können, insbesondere als Gegenstück zu einer Platzierung neu, 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 ist es, die Speicherzuordnung von der Objektkonstruktion abzukoppeln.

3
Stuart Golodetz

1) Objekte werden nicht 'über Zeiger' erstellt. Es gibt einen Zeiger, der jedem von Ihnen 'neuen' Objekt zugewiesen wird. Angenommen, dies ist das, was Sie meinen, wenn Sie 'delete' für den Zeiger aufrufen, wird das Objekt die Zeiger-Dereferenzierungen tatsächlich löschen (und den Destruktor aufrufen). Wenn Sie den Zeiger einem anderen Objekt zuweisen, tritt ein Speicherverlust auf. Nichts in C++ sammelt Ihren Müll für Sie.

2) Dies sind zwei getrennte Fragen. Eine Variable verlässt den Gültigkeitsbereich, wenn der Stack-Frame, in dem sie deklariert ist, vom Stack entfernt wird. Normalerweise ist dies der Fall, wenn Sie einen Block verlassen. Objekte in einem Haufen gehen niemals aus dem Bereich heraus, obwohl ihre Zeiger auf dem Stapel möglicherweise sind. Nichts Bestimmtes 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 vermuten lässt. Normalerweise möchten Sie jedoch Ihre "neuen" Schlüsselwörter mit Ihren "Lösch" -Schlüsselwörtern abgleichen und alles in Ihren Destruktor stecken, um sicherzustellen, dass sich diese ordnungsgemäß bereinigt. Wenn Sie dies nicht tun, kommentieren Sie den Destruktor mit spezifischen Anweisungen an alle Benutzer, die die Klasse verwenden, wie sie die Ressourcen dieses Objekts manuell bereinigen sollten.

3
Nathaniel Ford
  1. Zeiger - Reguläre Zeiger unterstützen RAII nicht. Ohne eine explizite delete gibt es Müll. Glücklicherweise hat C++ Auto Pointer , die das für Sie erledigen!

  2. Scope - Stellen Sie sich vor, wann eine Variable für Ihr Programm unsichtbar wird. Normalerweise ist dies am Ende von {block}, wie Sie darauf hinweisen.

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

2
chrisaycock

Wann immer Sie "neu" verwenden, das heißt, eine Adresse an einen Zeiger anhängen oder, um Speicherplatz auf dem Heap zu beanspruchen, 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 um Zeiger handelt, müssen Sie sie manuell löschen. 3. 3. Wenn der Speicherplatz von "neu" beansprucht wird.

1
cloudygoose

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 in Zeile "finish" ausgeführt wird.

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 nicht explizit vom Benutzer gelöscht wird oder eine gegeben wird neue Adresse vor dem Löschen ist der Speicherverlust aufgetreten. Das ist ein Fehler.

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

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

Tut mir leid für mein schlechtes Englisch!

0
wyx

Denken Sie daran, dass der Konstruktor eines Objekts sofort aufgerufen wird, nachdem der Speicher für dieses Objekt zugewiesen wurde, und der Destruktor aufgerufen wird, bevor der Speicher des Objekts freigegeben wird.

0
Sunny Khandare

Ja, ein Destruktor (a.k.a. dtor) wird aufgerufen, wenn ein Objekt den Gültigkeitsbereich verlässt, wenn es sich im Stapel befindet, oder wenn Sie delete in einem Zeiger auf ein Objekt aufrufen.

  1. Wenn der Zeiger über delete gelöscht wird, wird der Zeiger aufgerufen. Wenn Sie den Zeiger erneut zuweisen, ohne delete aufzurufen, wird ein Speicherverlust verursacht, da sich das Objekt irgendwo noch im Speicher befindet. Im letzteren Fall wird der Dtor nicht aufgerufen.

  2. Eine gute Implementierung der 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 abzulagern, oder den Gültigkeitsbereich selbst überschritten hat). Dies ist von der Implementierung abhängig.

  3. Ich bezweifle es, aber es würde mich nicht wundern, wenn da draußen ein seltsamer Umstand ist.

0
tnecniv