Когда вызывается деструктор C ++?

118

Основной вопрос: когда программа вызывает метод деструктора класса в C ++? Мне сказали, что он вызывается всякий раз, когда объект выходит за пределы области видимости или подвергаетсяdelete

Более конкретные вопросы:

1) Если объект создается с помощью указателя, и этот указатель позже удаляется или ему предоставляется новый адрес, на который он указывает, вызывает ли объект, на который он указывал, свой деструктор (при условии, что на него ничего не указывает)?

2) В продолжение вопроса 1, что определяет, когда объект выходит за пределы области видимости (не в отношении того, когда объект покидает данный {блок}). Итак, другими словами, когда деструктор вызывается для объекта в связанном списке?

3) Хотели бы вы когда-нибудь вызвать деструктор вручную?

Пэт Мюррей
источник
3
Даже ваши конкретные вопросы слишком широки. «Этот указатель позже удаляется» и «Дан новый адрес для указания» - совершенно разные вещи. Поищите больше (на некоторые из них уже даны ответы), а затем задайте отдельные вопросы для частей, которые вы не смогли найти.
Matthew Flaschen

Ответы:

74

1) Если объект создается с помощью указателя, и этот указатель позже удаляется или ему предоставляется новый адрес, на который он указывает, вызывает ли объект, на который он указывал, свой деструктор (при условии, что на него ничего не указывает)?

Это зависит от типа указателей. Например, интеллектуальные указатели часто удаляют свои объекты при удалении. Обычных указателей нет. То же самое верно, когда указатель указывает на другой объект. Некоторые интеллектуальные указатели уничтожат старый объект или уничтожат его, если на него больше не будет ссылок. У обычных указателей такой хитрости нет. Они просто содержат адрес и позволяют вам выполнять операции с объектами, на которые они указывают, специально для этого.

2) В продолжение вопроса 1, что определяет, когда объект выходит за пределы области видимости (не в отношении того, когда объект покидает данный {блок}). Итак, другими словами, когда деструктор вызывается для объекта в связанном списке?

Это до реализации связанного списка. Типичные коллекции при уничтожении уничтожают все содержащиеся в них объекты.

Таким образом, связанный список указателей обычно уничтожает указатели, но не объекты, на которые они указывают. (Что может быть правильным. Это могут быть ссылки других указателей.) Однако связанный список, специально разработанный для содержания указателей, может удалять объекты при собственном уничтожении.

Связанный список интеллектуальных указателей может автоматически удалять объекты при удалении указателей или делать это, если у них больше нет ссылок. Все зависит от вас, чтобы выбрать части, которые делают то, что вы хотите.

3) Хотели бы вы когда-нибудь вызвать деструктор вручную?

Конечно. Одним из примеров может быть ситуация, когда вы хотите заменить объект другим объектом того же типа, но не хотите освобождать память только для ее повторного выделения. Вы можете уничтожить старый объект на месте и построить новый на месте. (Однако в целом это плохая идея.)

// 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");
}
Дэвид Шварц
источник
2
Я думал, что последний из ваших примеров объявил функцию? Это пример «самого досадного разбора». (Другой, более тривиальный момент: я полагаю, вы имели в виду new Foo()с большой буквы.)
Стюарт Голодец
1
Я думаю, что Foo myfoo("foo")это не самый неприятный анализ, но он char * foo = "foo"; Foo myfoo(foo);есть.
Cosine
Это может быть глупый вопрос, но не следует ли его delete myFooзадавать раньше Foo *myFoo = new Foo("foo");? В противном случае вы бы удалили только что созданный объект, нет?
Matheus Rocha
Существует не myFooдо Foo *myFoo = new Foo("foo");линии. Эта строка создает новую переменную с именем myFoo, затеняя любую существующую. Хотя в этом случае его нет, так как myFooвышеупомянутое входит в сферу действия if, которая закончилась.
Дэвид Шварц
1
@galactikuh "Умный указатель" - это то, что действует как указатель на объект, но также имеет функции, которые упрощают управление временем жизни этого объекта.
Дэвид Шварц
20

Другие уже рассмотрели другие проблемы, поэтому я просто остановлюсь на одном моменте: вы когда-нибудь хотели удалить объект вручную.

Ответ положительный. @DavidSchwartz привел один пример, но он довольно необычный. Я приведу пример, который находится под капотом того, что многие программисты на C ++ используют все время: std::vectorstd::dequeхотя он используется не так часто).

Как известно большинству людей, std::vectorбудет выделять больший блок памяти, когда / если вы добавляете больше элементов, чем может вместить его текущее распределение. Однако, когда он это делает, у него есть блок памяти, способный хранить больше объектов, чем сейчас в векторе.

Чтобы управлять этим, то, что vectorскрывается, выделяет необработанную память через Allocatorобъект (что, если не указано иное, означает, что он использует ::operator new). Затем, когда вы используете (например), push_backчтобы добавить элемент в vector, внутри вектор использует a placement newдля создания элемента в (ранее) неиспользованной части его пространства памяти.

Теперь, что происходит, когда / если вы eraseполучаете элемент из вектора? Он не может просто использовать delete- это освободит весь его блок памяти; ему необходимо уничтожить один объект в этой памяти, не разрушая другие, или освобождая какой-либо блок памяти, который он контролирует (например, если у вас erase5 элементов из вектора, а затем сразу еще push_back5 элементов, гарантируется, что вектор не будет перераспределен память, когда вы это сделаете.

Для этого вектор напрямую уничтожает объекты в памяти, явно вызывая деструктор, а не используя delete.

Если, возможно, кто-то другой должен был написать контейнер, использующий непрерывное хранилище, примерно как это vectorделает (или какой-то его вариант, как на std::dequeсамом деле), вы почти наверняка захотите использовать тот же метод.

Например, давайте рассмотрим, как можно написать код для кольцевого буфера.

#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

В отличие от стандартных контейнеров, здесь используется operator newи operator deleteнапрямую. Для реального использования вы, вероятно, захотите использовать класс распределителя, но на данный момент он будет больше отвлекать, чем способствовать (во всяком случае, IMO).

Джерри Гроб
источник
9
  1. Когда вы создаете объект с помощью new, вы отвечаете за вызов delete. Когда вы создаете объект с помощью make_shared, результат shared_ptrотвечает за ведение счетчика и вызов, deleteкогда счетчик использования становится равным нулю.
  2. Выход из области действия означает выход из блока. Это когда вызывается деструктор, предполагая, что объект не был выделен new(т.е. это объект стека).
  3. Примерно единственный раз, когда вам нужно вызвать деструктор явно, - это когда вы выделяете объекту место размещенияnew .
dasblinkenlight
источник
1
Есть подсчет ссылок (shared_ptr), но явно не для простых указателей.
Pubby
1
@Pubby: Хороший момент, давайте продвигать хорошие практики. Отредактированный ответ.
MSalters
6

1) Объекты не создаются «с помощью указателей». Есть указатель, который назначается любому «новому» объекту. Предполагая, что это именно то, что вы имеете в виду, если вы вызываете 'delete' на указателе, он фактически удалит (и вызовет деструктор) объект, разыменованный указателем. Если присвоить указатель другому объекту, произойдет утечка памяти; ничто в C ++ не соберет ваш мусор за вас.

2) Это два отдельных вопроса. Переменная выходит за пределы области видимости, когда кадр стека, в котором она объявлена, извлекается из стека. Обычно это когда вы покидаете блок. Объекты в куче никогда не выходят за пределы области видимости, хотя их указатели в стеке могут. Ничто в частности не гарантирует, что деструктор объекта в связанном списке будет вызван.

3) Не совсем. Может быть Deep Magic, который предполагает иное, но обычно вы хотите сопоставить свои «новые» ключевые слова с ключевыми словами «удалить» и поместить в свой деструктор все, что необходимо, чтобы убедиться, что он должным образом очищается. Если вы этого не сделаете, обязательно прокомментируйте деструктор конкретными инструкциями для всех, кто использует класс, о том, как они должны очищать ресурсы этого объекта вручную.

Натаниэль Форд
источник
3

Чтобы дать подробный ответ на вопрос 3: да, есть (редкие) случаи, когда вы можете явно вызвать деструктор, в частности, как аналог нового размещения, как отмечает dasblinkenlight.

Чтобы привести конкретный пример этого:

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

Цель такого рода вещей - отделить выделение памяти от построения объекта.

Стюарт Голодец
источник
2
  1. Указатели - обычные указатели не поддерживают RAII. Без явного deleteбудет фигня. К счастью, в C ++ есть автоматические указатели, которые справятся с этим за вас!

  2. Область действия - подумайте о том, когда переменная становится невидимой для вашей программы. Обычно это в конце {block}, как вы указываете.

  3. Ручное уничтожение - никогда не пытайтесь это сделать. Просто позвольте прицелу и RAII творить чудеса за вас.

chrisaycock
источник
Примечание: auto_ptr устарел, как упоминается в вашей ссылке.
tnecniv
std::auto_ptrустарела в C ++ 11, да. Если OP действительно имеет C ++ 11, он должен использовать его std::unique_ptrдля отдельных владельцев или std::shared_ptrдля нескольких владельцев с подсчетом ссылок.
chrisaycock
«Ручное уничтожение - никогда не пытайтесь это сделать». Я очень часто помещаю указатели объектов в очередь в другой поток, используя системный вызов, который компилятор не понимает. «Использование» указателей scope / auto / smart привело бы к катастрофическому сбою моих приложений, поскольку объекты были удалены вызывающим потоком до того, как они могли быть обработаны потоком-потребителем. Эта проблема затрагивает объекты и интерфейсы с ограниченной областью действия и refCounted. Подойдут только указатели и явное удаление.
Мартин Джеймс
@MartinJames Можете ли вы опубликовать пример системного вызова, который компилятор не понимает? А как вы реализуете очередь? Нет, std::queue<std::shared_ptr>?я обнаружил, что pipe()параллелизм между потоком-производителем и потоком-потребителем становится намного проще, если копирование не слишком дорого.
chrisaycock
myObject = новый myClass (); PostMessage (aHandle, WM_APP, 0, LPPARAM (MyObject));
Мартин Джеймс
1

Каждый раз, когда вы используете «новый», то есть прикрепляете адрес к указателю или, скажем, претендуете на место в куче, вам нужно «удалить» его.
1. да, при удалении вызывается деструктор.
2. При вызове деструктора связанного списка вызывается деструктор его объектов. Но если это указатели, их нужно удалить вручную. 3. когда на место претендует «новый».

cloudygoose
источник
0

Да, деструктор (он же dtor) вызывается, когда объект выходит за пределы области видимости, если он находится в стеке, или когда вы вызываете deleteуказатель на объект.

  1. Если указатель удален через, deleteто будет вызван dtor. Если вы переназначаете указатель без предварительного вызова delete, вы получите утечку памяти, потому что объект все еще существует где-то в памяти. В последнем случае dtor не вызывается.

  2. Хорошая реализация связанного списка будет вызывать dtor всех объектов в списке, когда список уничтожается (потому что вы либо вызвали какой-то метод для его уничтожения, либо он сам вышел за пределы области видимости). Это зависит от реализации.

  3. Сомневаюсь, но не удивлюсь, если возникнут какие-то странные обстоятельства.

tnecniv
источник
1
«Если вы переназначаете указатель без предварительного вызова delete, вы получите утечку памяти, потому что объект все еще существует где-то в памяти». Не обязательно. Его можно было удалить с помощью другого указателя.
Matthew Flaschen
0

Если объект создается не с помощью указателя (например, A a1 = A ();), деструктор вызывается, когда объект разрушается, всегда когда функция, в которой находится объект, завершена. Например:

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


деструктор вызывается, когда код выполняется до строки "finish".

Если объект создается с помощью указателя (например, A * a2 = new A ();), деструктор вызывается при удалении указателя (delete a2;). Если точка не удалена пользователем явно или не задана новый адрес перед его удалением, происходит утечка памяти. Это ошибка.

В связанном списке, если мы используем std :: list <>, нам не нужно заботиться о деструкторе или утечке памяти, потому что std :: list <> завершил все это за нас. В связанном списке, написанном нами, мы должны написать десктруктор и удалить указатель явно, иначе это вызовет утечку памяти.

Мы редко вызываем деструктор вручную. Это функция, обеспечивающая систему.

Простите за мой плохой английский!

wyx
источник
Неверно, что вы не можете вызвать деструктор вручную - вы можете (см., Например, код в моем ответе). Что правда, так это то, что в подавляющем большинстве случаев вы не должны :)
Стюарт Голодец
0

Помните, что конструктор объекта вызывается сразу после выделения памяти для этого объекта, а деструктор вызывается непосредственно перед освобождением памяти этого объекта.

Солнечный Кхандаре
источник