Поскольку оператор удаления освобождает память, зачем мне деструктор?

Из часто задаваемых вопросов по С++: http://www.parashift.com/c++-faq-lite/dtors.html#faq-11.9

Помните: delete p делает две вещи: вызывает деструктор и освобождает память.

Если delete освобождает память, то зачем тут деструктор?


person Aquarius_Girl    schedule 03.02.2012    source источник
comment
Это ошибка сборки мусора: память — не единственный ресурс, который нуждается в очистке.   -  person R. Martinho Fernandes    schedule 03.02.2012
comment
@R.MartinhoFernandes Подробное объяснение было бы полезно. Один лайнер обычно не легко понять.   -  person Aquarius_Girl    schedule 03.02.2012
comment
Я говорю, что если бы удаление освобождало только память, оно не очищало бы другие ресурсы, такие как закрытие файлов, сетевые подключения или снятие блокировок.   -  person R. Martinho Fernandes    schedule 03.02.2012
comment
@AnishaKaul И представьте деструктор std::shared_ptr: он должен условно удалить удерживаемый указатель и уменьшить количество ссылок (возможно, в потокобезопасном режиме)   -  person sehe    schedule 03.02.2012
comment
Перед смертью мужчина может захотеть продать свою машину, расстаться с девушкой и отменить подписку на Wired Magazine. Рецепт, описывающий все эти необходимые шаги перед смертью, называется деструктором. Обратите внимание на часть before. Деструктор не включает в себя кончину самого человека, потому что ни один здравомыслящий человек не стал бы утверждать, что прежде чем смерть схватит свою вечную власть над моей душой, я хочу убедиться, что умру первым.   -  person fredoverflow    schedule 03.02.2012
comment
@FredOverflow Это хорошо. Но в деструкторе нам самим нужно делать дополнительную работу? правильно?   -  person Aquarius_Girl    schedule 03.02.2012
comment
Выделение памяти и построение объекта — это две отдельные концепции в C++. Освобождение относится к первому, разрушение — ко второму.   -  person Kerrek SB    schedule 03.02.2012
comment
Если простое уничтожение по элементам работает правильно (если ваши члены std::string и std::vector и т. д.), вам не нужно писать деструктор самостоятельно. Если он не работает правильно (если ваши члены char* или FILE* или кто-то еще), вы должны написать деструктор, делающий правильные вещи. (Обратите внимание, что удаление указателя ничего не делает.) Поэтому, если вы хотите выполнить delete member_pointer до того, как объект умрет, вы должны явно указать так в деструкторе. Подробности см. в этих часто задаваемых вопросах.   -  person fredoverflow    schedule 03.02.2012


Ответы (9)


Вам нужно вызвать деструктор, если есть другие вещи, которые необходимо сделать, кроме простого освобождения памяти.

Помимо очень простых классов, обычно они есть.

Такие вещи, как закрытие файловых дескрипторов или отключение соединений с базой данных, удаление других объектов, на которые указывают данные-члены в вашем объекте, и так далее.

Классический пример — реализация стека:

class myStack {
    private:
        int *stackData;
        int topOfStack;

    public:
        void myStack () {
            topOfStack = 0;
            stackData = new int[100];
        }

        void ~myStack () {
            delete [] stackData;
        }

        // Other stuff here like pop(), push() and so on.
}

Теперь подумайте, что произойдет, если деструктор не будет вызываться каждый раз при удалении одного из ваших стеков. В этом случае в C++ нет автоматической сборки мусора, поэтому stackData память будет утекать, и вы в конечном итоге закончите.


Это требование к деструктору удалить все свои ресурсы перемещает вниз по дереву к базовым типам. Например, у вас может быть пул соединений с базой данных с массивом соединений с базой данных. Деструктор для этого будет delete каждое отдельное соединение с базой данных.

Одно соединение с базой данных может выделять множество вещей, таких как буферы данных, кэши, скомпилированные запросы SQL и так далее. Таким образом, деструктор для подключения к базе данных также должен delete выполнять все эти действия.

Другими словами, у вас есть что-то вроде:

+-------------------------------------+
| DB connection pool                  |
|                                     |
| +-------------------------+---+---+ |
| | Array of DB connections |   |   | |
| +-------------------------+---+---+ |
|                             |   |   |
+-----------------------------|---|---+
                              |   |   +---------+
                              |   +-> | DB Conn |
             +---------+      |       +---------+
             | DB Conn | <----+         /  |  \
             +---------+         buffers   |   queries
               /  |  \                  caches
        buffers   |   queries
               caches

Освобождение памяти для пула соединений с БД не повлияет на существование отдельного соединения с БД или других объектов, на которые они указывают.

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

Класс вроде:

class intWrapper {
    private:
        int value;
    public:
        intWrapper () { value = 0; }
        ~intWrapper() {}
        void setValue (int newval) { value = newval; }
        int getValue (void) { return value; }
}

не имеет реальной необходимости в деструкторе, так как освобождение памяти — это все, что вам нужно сделать.


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

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

person paxdiablo    schedule 03.02.2012
comment
so the stackData memory would leak and you'd eventually run out. Ничего не понял. Удалить освобождает память, поэтому вызов удаления в стеке освободит ее, так как же происходят утечки памяти? - person Aquarius_Girl; 03.02.2012
comment
@ Аниша, деструктор полностью отвечает за уборку после себя. Это означает, что он также должен удалять любые динамически созданные объекты, которые он создал за время своего существования (обычно, но не всегда, в своем конструкторе). Действие по удалению его ресурсов может двигаться дальше по дереву. Смотрите мой обновленный ответ. Освобождение памяти для объекта myStack не освобождает выделенную им память, за это отвечает его деструктор. - person paxdiablo; 03.02.2012
comment
Итак, деструктор очищает выделенные дочерние объекты уничтоженных объектов? - person Aquarius_Girl; 03.02.2012
comment
@Anisha, да, хотя есть другие вещи (помимо простого вызова delete), которые может потребоваться выполнить в рамках этой очистки (например, закрытие устаревших библиотек C, которые не используют объекты). - person paxdiablo; 03.02.2012
comment
has no real need for a destructor since the memory deallocation is all you need to do. В этом классе вы не выделяли никакой памяти, поэтому мы не должны ничего освобождать. Деструктор все равно понадобится для уничтожения объекта intWrapper a;? Правильно? - person Aquarius_Girl; 04.02.2012
comment
Нет, вам не нужен деструктор, C++ просто освободит память - он неявно предоставляет деструктор, который ничего не делает, если вы не предоставите свой собственный. Вы должны понимать, что выделение/освобождение памяти и построение/уничтожение — это две разные вещи, просто новые и удаляемые используют их обе. Например, delete [] arr вызовет деструктор один раз для каждого объекта в массиве, но может освободить только один блок памяти. - person paxdiablo; 04.02.2012
comment
You need to understand that memory allocation/deallocation and construction/destruction are two different things, Ну, я действительно НЕ понимаю, насколько это разные вещи. Ах! Вы имеете в виду, что конструкция относится к руководству new, а выделение относится к таким объектам, как classX objX;? - person Aquarius_Girl; 04.02.2012
comment
@ Аниша, внимательно прочитай последние два абзаца моего ответа: new делает две вещи. Первый — выделить память (например, malloc в C) для вашего объекта, второй — вызвать конструктор, чтобы вы могли настроить объект. С другой стороны, delete также выполняет две вещи. Он вызывает деструктор, чтобы вы могли уничтожить свой объект, а затем он освобождает память (а-ля C's free). Я не уверен, что смогу объяснить это яснее, и комментарии заполняются, поэтому, если это все еще не ясно, вы можете подумать о том, чтобы задать новый вопрос только по этому аспекту. - person paxdiablo; 04.02.2012

Если delete освобождает память, то зачем тут деструктор?

Смысл деструктора в том, чтобы выполнить любую логику, необходимую для очистки после вашего объекта, например:

  • вызов удаления для других объектов, принадлежащих уничтожаемому объекту
  • правильное освобождение других ресурсов, таких как соединения с базой данных; файловые дескрипторы и тому подобное
person razlebe    schedule 03.02.2012
comment
Ах, так вы имеете в виду, что удаление удаляет определенный объект и вызывает деструктор, который удаляет дочерние элементы объекта, удаленного с помощью удаления? - person Aquarius_Girl; 03.02.2012
comment
@AnishaKaul: ну... не совсем так: действия обратные. Удалить сначала вызывает деструктор (который делает то, что вы кодируете, например, удаляете что-то еще, закрываете файл или что-то еще), ТОГДА возвращает память объекта системе. - person Emilio Garavaglia; 03.02.2012
comment
@EmilioGaravaglia Если я не напишу код в деструкторе, он не удалит дочерние элементы удаленного объекта? - person Aquarius_Girl; 03.02.2012
comment
@AnishaKaul: да, нет. Концепция потомков объекта известна вам, а не C++. C++ просто знает, что есть указатели на что-то еще. Должны ли они быть также удалены или оставлены на своем месте (потому что используются также кем-то другим) — это что-то, принадлежащее вашей логике, что неизвестно компилятору, если вы не укажете это. Способ указать это код деструктора. Если вы хотите, вы можете думать о деструкторах как об обработчиках событий, которые запускаются при удалении. - person Emilio Garavaglia; 03.02.2012
comment
@EmilioGaravaglia Вау, так можно ли использовать деструктор после удаления? мы все равно должны писать код сами, чтобы мы могли писать его где угодно? - person Aquarius_Girl; 03.02.2012
comment
@Anisha: Если ваш объект управляет объектами, размещенными в куче (теми, которые созданы с помощью new), и вы хотите, чтобы они удалялись вместе с вашим объектом, вы должны удалить их в своем деструкторе. См. ответ paxdiablos. - person Stephan; 03.02.2012

Предположим, у вас есть класс, который динамически выделяет память:

class something {
public:
    something() {
        p = new int;
    }

    ~something() {
        delete p;
    }

    int *p;
};

Теперь давайте динамически выделим объект something:

something *s = new something();

delete s;

Теперь, если бы delete не вызывал деструктор, то s->p никогда не освободился бы. Таким образом, delete должен вызвать деструктор и затем освободить память.

person flight    schedule 03.02.2012
comment
Ах, это мило. Итак, Ах, так вы имеете в виду, что удаление удаляет определенный объект и вызывает деструктор, который удаляет дочерние элементы объекта, удаленного с помощью удаления? - person Aquarius_Girl; 03.02.2012
comment
Означает ли это также, что после явного удаления родительского объекта нет шансов на утечку памяти из-за родительских дочерних элементов? - person Aquarius_Girl; 03.02.2012
comment
Для первого вопроса он вызывает деструктор first, а затем удаляет объект. Потому что, очевидно, если вы сначала удалите объект, то как вы сможете его уничтожить? - person flight; 03.02.2012
comment
Для второго: Не обязательно. Удаление родительского объекта гарантирует только вызов деструктора. Если вы испортили свой конструктор, он не будет правильно удален, но если вы правильно сделали свой деструктор, то да, нет никаких шансов на утечку памяти. - person flight; 03.02.2012

Деструктор отвечает за освобождение ресурсов, отличных от выделенной памяти объекта. Например, если объект имеет открытый дескриптор файла, деструктор может вызвать для него fclose.

person StilesCrisis    schedule 03.02.2012
comment
Не выделенная память объекта. Память, выделенная ДЛЯ объекта - person Emilio Garavaglia; 03.02.2012

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

Тоже в общем... FAQ... обычно не ошибаются.

person Brian Roach    schedule 03.02.2012

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

person Riskhan    schedule 03.02.2012

Деструктор предназначен для очистки изменений, которые конструктор объекта и функции-члены могли внести в состояние программы. Это может быть что угодно — удалить объект из какого-то глобального списка, закрыть открытый файл, освободить выделенную память, закрыть соединение с базой данных и т. д.

person sharptooth    schedule 03.02.2012

Деструктор не был бы обязательной функцией. Такие языки, как C, Java, C#, не имеют деструкторов. C++ также может жить без него.

Деструктор — это специальное средство, предоставляемое C++ (такое же, как конструктор). Он вызывается, когда объект "уничтожается".

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

A* foo ()
{
  static A obj;  // 'A' is some class
  A *p = &obj;
  return p;
}

В приведенном выше коде obj представляет собой static данные, созданные типа A; foo() возвращает ссылку на этот obj, что нормально, потому что obj.~A() еще не вызывается. Предположим, что obj не является статическим. Код будет скомпилирован, однако A*, возвращенный foo(), теперь указывает на ячейку памяти, которая больше не является объектом A. Значит -> операция плохая/незаконная.

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

Также помните, что деструктор можно вызывать в нескольких местах:

int bar ()
{
  A obj;
  ...
  return 0; // obj.~A() called here
  ...
  return 1; // obj.~A() called here
  ...
  return 2; // obj.~A() called here
}

В приведенном выше примере obj.~A() будет вызываться только один раз, но его можно вызвать из любого из показанных мест.

Во время разрушения вы можете захотеть сделать несколько полезных вещей. Допустим, class A вычисляет какой-то результат, когда объект уничтожается; он должен напечатать результат вычисления. Это можно сделать в стиле C (поместив некоторую функцию в каждый оператор return). Но ~A() — это доступное универсальное средство.

person iammilind    schedule 03.02.2012
comment
but it can be called from any of the places shown Как это возможно? Когда встречается return 0, функция возвращается, и оставшийся код НИКОГДА не может быть выполнен, не так ли? - person Aquarius_Girl; 03.02.2012
comment
@AnishaKaul, да, в том-то и дело. obj.~A() вызывается только один раз, но компилятор выдает его код во всех показанных выше местах. Потому что компилятор не знает, что во время выполнения будет выполняться return. - person iammilind; 03.02.2012

В дополнение к ответам, сосредоточенным на объектах, выделенных в куче (с помощью new; которые освобождаются только с помощью delete)... Не забывайте, что если вы поместите объект в стеке (то есть без использования new), его деструктор будет вызван автоматически и он будет удален из стека (без вызова delete ), когда он выходит за рамки. Таким образом, у вас есть одна функция, которая гарантировано будет выполнена, когда объект выходит из области видимости, и это идеальное место для выполнения очистки всех других ресурсов, выделенных объектом (различные дескрипторы, сокеты... и объекты, созданные в куче этим объектом - если они не должны пережить этот). Это используется в идиоме RAII.

person Bojan Komazec    schedule 03.02.2012