Коли викликається деструктор C ++?


118

Основне запитання: коли програма викликає метод деструктора класу в C ++? Мені сказали, що він називається всякий раз, коли об'єкт виходить за межі сфери дії або піддається йомуdelete

Більш конкретні питання:

1) Якщо об’єкт створений за допомогою вказівника, а цей вказівник пізніше видаляється або йому надається нова адреса, на яку слід вказувати, чи об'єкт, на який він вказував, викликав свого деструктора (якщо припустити, що на нього нічого іншого не вказується)?

2) Слідкуючи за першим питанням, що визначає, коли об’єкт виходить із сфери дії (не стосується того, коли об’єкт залишає заданий {блок}). Отже, інакше кажучи, коли деструктор викликається на об'єкт у пов'язаному списку?

3) Чи хотіли б ви коли-небудь викликати деструктора вручну?


3
Навіть ваші конкретні запитання занадто широкі. "Цей покажчик пізніше видаляється" і "дається нова адреса, на яку слід вказати", зовсім інші. Шукайте більше (на деякі з них відповіли), а потім задайте окремі запитання щодо частин, яких ви не змогли знайти.
Метью Флашен

Відповіді:


74

1) Якщо об’єкт створений за допомогою вказівника, а цей вказівник пізніше видаляється або йому надається нова адреса, на яку слід вказувати, чи об'єкт, на який він вказував, викликав свого деструктора (якщо припустити, що на нього нічого іншого не вказується)?

Це залежить від типу покажчиків. Наприклад, розумні покажчики часто видаляють свої об'єкти, коли вони видаляються. Звичайні покажчики цього не роблять. Те саме відбувається, коли робиться покажчик, який вказує на інший об'єкт. Деякі розумні покажчики знищать старий об’єкт або знищать його, якщо в ньому більше не буде посилань. У звичайних покажчиків таких розумних немає. Вони просто містять адресу і дозволяють вам виконувати операції над об'єктами, на які вони вказують, спеціально роблячи це.

2) Слідкуючи за першим питанням, що визначає, коли об’єкт виходить із сфери дії (не стосується того, коли об’єкт залишає заданий {блок}). Отже, інакше кажучи, коли деструктор викликається на об'єкт у пов'язаному списку?

Це залежить від реалізації пов'язаного списку. Типові колекції знищують усі їх містяться предмети, коли вони знищуються.

Отже, пов'язаний список покажчиків зазвичай знищує вказівники, але не об'єкти, на які вони вказують. (Що може бути правильним. Вони можуть бути посиланнями інших покажчиків.) Зв'язаний список, спеціально розроблений для розміщення покажчиків, однак може видалити об'єкти при власному знищенні.

Зв'язаний список розумних покажчиків може автоматично видаляти об'єкти, коли покажчики видаляються, або робити це, якщо у них більше не було посилань. Все залежить від того, щоб вибрати шматочки, які роблять те, що ви хочете.

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()з великої літери "F".)
Стюарт Голодець,

1
Я думаю, що Foo myfoo("foo")це не самий вестинг-розбір, але char * foo = "foo"; Foo myfoo(foo);є.
Косін

Це може бути дурним питанням, але чи не delete myFooварто його називати раніше Foo *myFoo = new Foo("foo");? Або б ви видалили новостворений об’єкт, ні?
Матей Роша

Існує не 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 .

1
Є підрахунок посилань (shared_ptr), хоча, очевидно, не для звичайних покажчиків.
Паббі

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 зробити магію за вас.


Примітка. Auto_ptr застаріло, як згадується ваше посилання.
tnecniv

std::auto_ptrзастаріло в C ++ 11, так. Якщо в ОП насправді є C ++ 11, він повинен використовуватись std::unique_ptrдля одноосібників або std::shared_ptrдля декількох власників, що рахуються посиланнями.
chrisaycock

"Ручне знищення - ніколи цього не намагайтеся". Я дуже часто вибиваюся з черги на вказівники об'єктів на інший потік, використовуючи системний виклик, який компілятор не розуміє. "Покладання" на область показів / авто / розумні покажчики призведе до катастрофічного збою моїх додатків, оскільки об'єкти були видалені викликовим потоком перед тим, як їх обробляти споживчий потік. Це питання стосується об'єктів та інтерфейсів з обмеженою сферою дії та перерахуванням. Здійснюватимуться лише вказівники та явне видалення.
Мартін Джеймс

@MartinJames Чи можете ви розмістити приклад системного виклику, який компілятор не розуміє? А як ви реалізуєте чергу? Чи не std::queue<std::shared_ptr>?я виявив , що pipe()між виробником і споживачем потоків макіяжем паралельності набагато простіше, якщо копіювання не дуже дорого.
chrisaycock

myObject = новий мій клас (); PostMessage (aHandle, WM_APP, 0, LPPARAM (myObject));
Мартін Джеймс

1

Щоразу, коли ви використовуєте "нове", тобто додаєте адресу до вказівника, або, скажімо, ви вимагаєте пробіл у купі, потрібно "видалити" його.
1. так, коли ви щось видаляєте, викликається деструктор.
2. Коли викликається деструктор зв'язаного списку, викликається деструктор об'єктів. Але якщо вони є вказівниками, їх потрібно видалити вручну. 3. коли простір вимагається "новим".


0

Так, деструктор (він же dtor) викликається, коли об’єкт виходить за межі, якщо він знаходиться в стеці або коли ви викликаєте deleteвказівник на об'єкт.

  1. Якщо вказівник буде видалено через, deleteтоді буде викликаний dtor. Якщо переназначити вказівник, не викликаючи deleteспочатку, ви отримаєте витік пам'яті, оскільки об’єкт все ще десь є в пам'яті. В останньому випадку dtor не викликається.

  2. Хороша реалізація пов'язаного списку викличе dtor усіх об'єктів у списку, коли список знищується (тому що ви або викликали якийсь метод, щоб призначити його, або він вийшов із сфери застосування). Це залежить від реалізації.

  3. Я сумніваюся в цьому, але я не здивуюсь, якщо там є якась дивна обставина.


1
Msgstr "Якщо ви перепризначите вказівник, не вимагаючи видалення спочатку, ви отримаєте витік пам'яті, оскільки об'єкт десь існує в пам'яті." Не обов'язково. Його можна було видалити через інший покажчик.
Метью Флашен

0

Якщо об'єкт створений не за допомогою вказівника (наприклад, A a1 = A ();), деструктор викликається, коли об’єкт знищений, завжди, коли функція, де лежить об'єкт, закінчена. Наприклад:

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


деструктор викликається при виконанні коду до рядка "закінчити".

Якщо об'єкт створено за допомогою вказівника (наприклад, A * a2 = new A ();), деструктор викликається при видаленні покажчика (видалити a2;) Якщо точка не видаляється користувачем explictly або задається a нову адресу перед видаленням, відбувається витік пам'яті. Це помилка.

У зв'язаному списку, якщо ми використовуємо std :: list <>, нам не потрібно дбати про деструктор або витік пам'яті, оскільки std :: list <> закінчив усе це для нас. У пов'язаному списку, написаному нами самим, нам слід написати дектруктор і видалити покажчик експліцитно. Інакше це призведе до витоку пам'яті.

Ми рідко викликаємо деструктор вручну. Це функція, що забезпечує систему.

Вибачте за мою бідну англійську!


Неправда, що ви не можете викликати деструктора вручну - ви можете (див. Код, наприклад, у моїй відповіді). Що правда, це те, що переважна більшість часу ви не повинні :)
Стюарт Голодець,

0

Пам'ятайте, що Конструктор об'єкта викликається відразу після виділення пам'яті для цього об'єкта, тоді як деструктор викликається безпосередньо перед розгортанням пам'яті цього об'єкта.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.