Kiedy wywoływany jest destruktor C ++?

118

Podstawowe pytanie: kiedy program wywołuje metodę destruktora klasy w C ++? Powiedziano mi, że jest wywoływana za każdym razem, gdy obiekt wychodzi poza zakres lub jest poddawany działaniudelete

Bardziej szczegółowe pytania:

1) Jeśli obiekt jest tworzony za pomocą wskaźnika i ten wskaźnik jest później usuwany lub otrzymuje nowy adres do wskazywania, czy obiekt, na który wskazywał, ma wywołać swój destruktor (zakładając, że nic innego nie wskazuje na niego)?

2) Kontynuując pytanie 1, co definiuje, kiedy obiekt wychodzi poza zakres (nie dotyczy to, kiedy obiekt opuszcza dany {blok}). Innymi słowy, kiedy destruktor jest wywoływany na obiekcie na połączonej liście?

3) Czy kiedykolwiek chciałbyś ręcznie wywołać destruktor?

Pat Murray
źródło
3
Nawet twoje konkretne pytania są zbyt szerokie. „Ten wskaźnik zostanie później usunięty” i „otrzymanie nowego adresu, na który ma wskazywać”, są zupełnie inne. Wyszukaj więcej (na niektóre z nich udzielono odpowiedzi), a następnie zadaj oddzielne pytania dotyczące części, których nie możesz znaleźć.
Matthew Flaschen,

Odpowiedzi:

74

1) Jeśli obiekt jest tworzony za pomocą wskaźnika i ten wskaźnik jest później usuwany lub otrzymuje nowy adres do wskazywania, czy obiekt, na który wskazywał, ma wywołać swój destruktor (zakładając, że nic innego nie wskazuje na niego)?

To zależy od rodzaju wskaźników. Na przykład inteligentne wskaźniki często usuwają swoje obiekty po ich usunięciu. Zwykłe wskazówki nie. To samo dotyczy sytuacji, gdy wskaźnik wskazuje inny obiekt. Niektóre inteligentne wskaźniki zniszczą stary obiekt lub zniszczą go, jeśli nie ma już odniesień. Zwykłe wskaźniki nie mają takiej sprytu. Po prostu przechowują adres i umożliwiają wykonywanie operacji na obiektach, na które wskazują, w konkretny sposób.

2) Kontynuując pytanie 1, co definiuje, kiedy obiekt wychodzi poza zakres (nie dotyczy to, kiedy obiekt opuszcza dany {blok}). Innymi słowy, kiedy destruktor jest wywoływany na obiekcie na połączonej liście?

To zależy od implementacji połączonej listy. Typowe kolekcje niszczą wszystkie zawarte w nich przedmioty, gdy są niszczone.

Tak więc połączona lista wskaźników zwykle zniszczyłaby wskaźniki, ale nie obiekty, na które wskazują. (Co może być poprawne. Mogą to być odniesienia z innych wskaźników). Połączona lista zaprojektowana specjalnie do zawierania wskaźników może jednak spowodować usunięcie obiektów w wyniku własnego zniszczenia.

Połączona lista inteligentnych wskaźników może automatycznie usuwać obiekty, gdy wskaźniki są usuwane, lub robić to, jeśli nie mają więcej odniesień. Wszystko zależy od Ciebie, aby wybrać elementy, które zrobią to, co chcesz.

3) Czy kiedykolwiek chciałbyś ręcznie wywołać destruktor?

Pewnie. Jednym z przykładów może być zastąpienie obiektu innym obiektem tego samego typu, ale nie chcesz zwolnić pamięci tylko po to, aby ją ponownie przydzielić. Możesz zniszczyć stary obiekt w miejscu i zbudować nowy na miejscu. (Jednak ogólnie jest to zły pomysł).

// 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
źródło
2
Myślałem, że ostatni z twoich przykładów zadeklarował funkcję? To przykład „najbardziej irytującej analizy”. (Inną, bardziej trywialną kwestią jest to, że myślę, że miałeś na myśli new Foo()duże „F”.)
Stuart Golodetz
1
Myślę, że Foo myfoo("foo")nie jest to najbardziej irytująca analiza, ale tak char * foo = "foo"; Foo myfoo(foo);jest.
Cosinus
To może być głupie pytanie, ale czy nie należy delete myFoogo wcześniej zadawać Foo *myFoo = new Foo("foo");? Albo możesz usunąć nowo utworzony obiekt, nie?
Matheus Rocha
Nie ma myFooprzed Foo *myFoo = new Foo("foo");linią. Ta linia tworzy zupełnie nową zmienną o nazwie myFoo, przesłaniając każdą istniejącą. Chociaż w tym przypadku nie istnieje, ponieważ myFoopowyższe znajduje się w zakresie if, który się zakończył.
David Schwartz
1
@galactikuh „Inteligentny wskaźnik” to coś, co działa jak wskaźnik do obiektu, ale ma również funkcje, które ułatwiają zarządzanie czasem życia tego obiektu.
David Schwartz,
20

Inni już zajęli się innymi problemami, więc przyjrzę się tylko jednej kwestii: czy kiedykolwiek chcesz ręcznie usunąć obiekt.

Odpowiedź brzmi tak. @DavidSchwartz podał jeden przykład, ale jest on dość nietypowy. Podam przykład, który jest pod maską tego, z czego wielu programistów C ++ korzysta cały czas: std::vector(i std::dequechociaż nie jest używany tak często).

Jak większość ludzi wie, std::vectorprzydzieli większy blok pamięci, gdy / jeśli dodasz więcej elementów, niż może pomieścić obecna alokacja. Jednak gdy to robi, ma blok pamięci, który może pomieścić więcej obiektów niż obecnie znajduje się w wektorze.

Aby sobie z tym poradzić, to, co vectorrobi pod osłonami, polega na przydzielaniu pamięci surowej za pośrednictwem Allocatorobiektu (który, o ile nie określono inaczej, oznacza, że ​​używa ::operator new). Następnie, gdy użyjesz (na przykład), push_backaby dodać element do vector, wewnętrznie wektor używa a, placement newaby utworzyć element w (wcześniej) nieużywanej części swojej przestrzeni pamięci.

Co się dzieje, gdy / jeśli jesteś eraseelementem z wektora? Nie może po prostu używać delete- to zwolniłoby cały blok pamięci; musi zniszczyć jeden obiekt w tej pamięci bez niszczenia innych lub zwolnienia żadnego z bloków pamięci, które kontroluje (na przykład, jeśli masz erase5 elementów z wektora, a następnie natychmiast push_back5 więcej elementów, to gwarantuje, że wektor nie zostanie ponownie przydzielony pamięć, kiedy to robisz.

Aby to zrobić, wektor bezpośrednio niszczy obiekty w pamięci, jawnie wywołując destruktor, a nie używając delete.

Jeśli, być może, ktoś inny napisałby kontener przy użyciu ciągłej pamięci, mniej więcej tak, jak vectorrobi (lub jakiś jej wariant, jak std::dequenaprawdę), prawie na pewno chciałbyś użyć tej samej techniki.

Na przykład zastanówmy się, jak możesz napisać kod dla okrągłego bufora pierścieniowego.

#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;
    }
  
~circular_buffer() {
    // first destroy any content
    while (in_use != 0)
        pop();

    // then release the buffer.
    operator delete(data); 
}

};

#endif

W przeciwieństwie do standardowych pojemników, to używa operator newi operator deletebezpośrednio. W przypadku rzeczywistego użytku prawdopodobnie zechcesz użyć klasy alokatora, ale w tej chwili bardziej rozpraszałoby to niż wnosiło wkład (w każdym razie IMO).

Jerry Coffin
źródło
9
  1. Kiedy tworzysz obiekt za pomocą new, jesteś odpowiedzialny za wywołanie delete. Kiedy tworzysz obiekt za pomocą make_shared, wynik shared_ptrjest odpowiedzialny za utrzymywanie licznika i wywoływanie, deletegdy licznik użycia spadnie do zera.
  2. Wyjście poza zasięg oznacza pozostawienie bloku. Dzieje się tak, gdy wywoływany jest destruktor, zakładając, że obiekt nie został przydzielony new(tj. Jest to obiekt stosu).
  3. Prawie jedyny przypadek, w którym trzeba jawnie wywołać destruktor, ma miejsce w przypadku przydzielenia obiektowi miejsca docelowegonew .
dasblinkenlight
źródło
1
Istnieje liczenie odwołań (shared_ptr), chociaż oczywiście nie jest to zwykłe wskaźniki.
Pubby
1
@Pubby: Słuszna uwaga, promujmy dobre praktyki. Edytowana odpowiedź.
MSalters
6

1) Obiekty nie są tworzone „za pomocą wskaźników”. Istnieje wskaźnik, który jest przypisany do każdego „nowego” obiektu. Zakładając, że to masz na myśli, jeśli wywołasz „delete” na wskaźniku, to faktycznie usunie (i wywoła destruktor na) obiekt, do którego wskaźnik usuwa. Jeśli przypiszesz wskaźnik do innego obiektu, nastąpi przeciek pamięci; nic w C ++ nie zbierze dla Ciebie śmieci.

2) To są dwa oddzielne pytania. Zmienna wychodzi poza zakres, gdy ramka stosu, w której jest zadeklarowana, zostaje zdjęta ze stosu. Zwykle dzieje się tak, gdy opuszczasz blok. Obiekty w stercie nigdy nie wychodzą poza zasięg, chociaż ich wskaźniki na stosie mogą. Nic w szczególności nie gwarantuje, że zostanie wywołany destruktor obiektu z połączonej listy.

3) Niezupełnie. Może istnieć Deep Magic, który sugerowałby coś innego, ale zazwyczaj chcesz dopasować swoje „nowe” słowa kluczowe do słów kluczowych „usuń” i umieścić w destruktorze wszystko, co niezbędne, aby upewnić się, że prawidłowo się wyczyści. Jeśli tego nie zrobisz, pamiętaj, aby skomentować destruktor, podając szczegółowe instrukcje każdemu, kto używa tej klasy, w jaki sposób powinni ręcznie wyczyścić zasoby tego obiektu.

Nathaniel Ford
źródło
3

Aby udzielić szczegółowej odpowiedzi na pytanie 3: tak, są (rzadkie) sytuacje, w których możesz wywołać destruktor jawnie, w szczególności jako odpowiednik nowego miejsca, jak zauważa dasblinkenlight.

Aby podać konkretny przykład:

#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;
}

Celem tego rodzaju rzeczy jest oddzielenie alokacji pamięci od konstrukcji obiektu.

Stuart Golodetz
źródło
2
  1. Wskaźniki - zwykłe wskaźniki nie obsługują RAII. Bez wyraźnego deletebędzie śmieci. Na szczęście C ++ ma automatyczne wskaźniki, które obsługują to za Ciebie!

  2. Zakres - Pomyśl o tym, kiedy zmienna staje się niewidoczna dla twojego programu. Zwykle jest to koniec {block}, jak zauważyłeś.

  3. Ręczne niszczenie - nigdy tego nie próbuj. Po prostu pozwól, aby zakres i RAII zrobiły za Ciebie magię.

chrisaycock
źródło
Uwaga: auto_ptr jest przestarzałe, o czym wspomina Twój link.
tnecniv
std::auto_ptrjest przestarzała w C ++ 11, tak. Jeśli OP faktycznie ma C ++ 11, powinien używać go std::unique_ptrdla pojedynczych właścicieli lub std::shared_ptrdla wielu właścicieli liczonych jako referencje.
chrisaycock
„Ręczne niszczenie - nigdy tego nie próbuj”. Bardzo często ustawiam wskaźniki obiektów w kolejce do innego wątku, używając wywołania systemowego, którego kompilator nie rozumie. `` Oparcie się '' na wskaźnikach zakresu / auto / inteligentnych spowodowałoby katastrofalne awarie moich aplikacji, ponieważ obiekty zostały usunięte przez wątek wywołujący, zanim mogły zostać obsłużone przez wątek konsumenta. Ten problem dotyczy obiektów i interfejsów z ograniczeniem zakresu i refCounted. Zrobią to tylko wskaźniki i jawne usuwanie.
Martin James,
@MartinJames Czy możesz opublikować przykład wywołania systemowego, którego kompilator nie rozumie? A jak wdrażasz kolejkę? Nie std::queue<std::shared_ptr>?zauważyłem, że pipe()między wątkiem producenta i konsumenta współbieżność jest o wiele łatwiejsza, jeśli kopiowanie nie jest zbyt drogie.
chrisaycock
myObject = new myClass (); PostMessage (aHandle, WM_APP, 0, LPPARAM (myObject));
Martin James,
1

Za każdym razem, gdy używasz słowa „nowy”, czyli dołączasz adres do wskaźnika, lub mówiąc, że zajmujesz miejsce na stercie, musisz go „usunąć”.
1. tak, kiedy coś usuniesz, wywoływany jest destruktor.
2.Wywołanie destruktora listy połączonej powoduje wywołanie destruktora obiektów. Ale jeśli są to wskaźniki, musisz je usunąć ręcznie. 3. gdy miejsce jest określane jako „nowe”.

mętna gęś
źródło
0

Tak, destruktor (inaczej dtor) jest wywoływany, gdy obiekt wychodzi poza zasięg, jeśli znajduje się na stosie lub gdy wywołujesz deletewskaźnik do obiektu.

  1. Jeśli wskaźnik zostanie usunięty za pośrednictwem, deletezostanie wywołany dtor. Jeśli zmienisz przypisanie wskaźnika bez wcześniejszego wywołania delete, otrzymasz wyciek pamięci, ponieważ obiekt nadal istnieje gdzieś w pamięci. W tym drugim przypadku dtor nie jest wywoływany.

  2. Dobra implementacja listy połączonej wywoła dtor wszystkich obiektów na liście, gdy lista jest niszczona (ponieważ albo wywołałeś jakąś metodę, aby ją zniszczyć, albo sama wyszła poza zakres). Zależy to od implementacji.

  3. Wątpię, ale nie zdziwiłbym się, gdyby zaszły jakieś dziwne okoliczności.

tnecniv
źródło
1
„Jeśli zmienisz przypisanie wskaźnika bez wcześniejszego wywołania funkcji delete, wystąpi wyciek pamięci, ponieważ obiekt nadal istnieje gdzieś w pamięci.”. Niekoniecznie. Mogło zostać usunięte za pomocą innego wskaźnika.
Matthew Flaschen,
0

Jeśli obiekt jest tworzony nie za pomocą wskaźnika (na przykład A a1 = A ();), destruktor jest wywoływany po zniszczeniu obiektu, zawsze wtedy, gdy funkcja, na której leży obiekt, jest zakończona. Na przykład:

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


Destruktor jest wywoływany, gdy kod jest wykonywany w wierszu „finish”.

Jeśli obiekt jest tworzony za pomocą wskaźnika (na przykład A * a2 = new A ();), destruktor jest wywoływany po usunięciu wskaźnika (delete a2;). Jeśli punkt nie zostanie usunięty przez użytkownika jawnie lub otrzymany nowy adres przed jego usunięciem, wystąpił wyciek pamięci. To jest błąd.

Na połączonej liście, jeśli używamy std :: list <>, nie musimy przejmować się desktruktorem lub wyciekiem pamięci, ponieważ std :: list <> zakończyło to wszystko za nas. Na utworzonej przez nas połączonej liście powinniśmy napisać desktruktor i jawnie usunąć wskaźnik, w przeciwnym razie spowoduje to wyciek pamięci.

Rzadko nazywamy destruktor ręcznie. Jest to funkcja zapewniająca system.

Przepraszam za mój słaby angielski!

wyx
źródło
To nieprawda, że ​​nie możesz ręcznie wywołać destruktora - możesz (zobacz na przykład kod w mojej odpowiedzi). Prawdą jest, że większość czasu nie należy :)
Stuart Golodetz
0

Pamiętaj, że Konstruktor obiektu jest wywoływany natychmiast po przydzieleniu pamięci dla tego obiektu, a destruktor jest wywoływany tuż przed zwolnieniem pamięci tego obiektu.

Sunny Khandare
źródło