Что происходит при двойном удалении? - PullRequest
19 голосов
/ 07 февраля 2012
Obj *op = new Obj;
Obj *op2 = op;
delete op;
delete op2; // What happens here?

Что хуже всего может случиться, если вы случайно удалите дважды?Это имеет значение?Собирается ли компилятор выдать ошибку?

Ответы [ 7 ]

22 голосов
/ 07 февраля 2012

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

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

21 голосов
/ 07 февраля 2012

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

17 голосов
/ 07 февраля 2012

Это неопределенное поведение, поэтому фактический результат будет зависеть от среды компилятора и среды выполнения.

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

В рамках системы любой менеджер памяти должен поддерживать некоторые метаданные о каждом блоке данных, которые он выделяет, таким образом, чтобы он мог искать метаданные по указателю, возвращенному malloc / new. Обычно это принимает форму структуры с фиксированным смещением перед выделенным блоком. Эта структура может содержать «магическое число» - константу, которая вряд ли произойдет по чистой случайности. Если менеджер памяти видит магическое число в ожидаемом месте, он знает, что указатель, предоставленный для освобождения / удаления, скорее всего действителен. Если он не видит магическое число или видит другое число, означающее «этот указатель был недавно освобожден», он может либо молча проигнорировать запрос на бесплатное выполнение, либо распечатать полезное сообщение и прервать его. Любой является законным в соответствии со спецификацией, и существуют аргументы за / против для любого подхода.

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

Давайте попробуем. Превратите ваш код в законченную программу в so.cpp:

class Obj
{
public:
    int x;
};

int main( int argc, char* argv[] )
{
    Obj *op = new Obj;
    Obj *op2 = op;
    delete op;
    delete op2;

    return 0;
}

Скомпилируйте его (я использую gcc 4.2.1 на OSX 10.6.8, но YMMV):

russell@Silverback ~: g++ so.cpp

Запустите его:

russell@Silverback ~: ./a.out
a.out(1965) malloc: *** error for object 0x100100080: pointer being freed was not allocated
*** set a breakpoint in malloc_error_break to debug
Abort trap

Смотри, среда выполнения gcc на самом деле обнаруживает, что это было двойное удаление, и довольно полезна перед сбоем.

4 голосов
/ 07 февраля 2012

Компилятор может выдавать предупреждение или что-то, особенно в очевидном (как в вашем примере), но он не всегда может обнаружить.(Вы можете использовать что-то вроде valgrind, который может обнаружить это во время выполнения).Что касается поведения, это может быть что угодно.Некоторая безопасная библиотека может проверить и нормально с ней справиться - но другие среды выполнения (для скорости) сделают предположение, что вы называете это правильно (а это не так), а затем вылетят или хуже.Время выполнения позволяет предположить, что вы не удаляете дважды (даже если двойное удаление приведет к чему-то плохому, например, сбой вашего компьютера)

2 голосов
/ 05 июля 2016

Пока это не определено:

int* a = new int;
delete a;
delete a; // same as your code

это хорошо определено:

int* a = new int;
delete a;
a = nullptr; // or just NULL or 0 if your compiler doesn't support c++11
delete a; // nothing happens!

Думаю, я должен опубликовать это, так как никто больше не упоминал об этом.

1 голос
/ 21 ноября 2015

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

Стандартный универсальный ответ: все может случиться, это не совсем так. Например, компьютер не будет пытаться убить вас за это (если вы не программируете AI для робота):)

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

Но вот что "грубо" происходит в большинстве случаев:

delete состоит из 2 основных операций:

  • вызывает деструктор, если он определен
  • это как-то освобождает память, выделенную для объекта

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

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

Память должна быть свободна. Как это сделать, зависит от реализации в компиляторе, но он может также выполнять некоторую free подобную функцию, давая ей указатель и размер вашего объекта. Вызов free для уже удаленной памяти может привести к сбою, поскольку память может больше не принадлежать вам. Если он принадлежит вам, он может не сразу аварийно завершить работу, но он может перезаписать память, которая уже была выделена для какого-либо другого объекта вашей программы.

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

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

Вот пример кода, который является неправильным, но может также хорошо работать (он работает нормально с GCC на Linux):

class a {};

int main()
{
    a *test = new a();
    delete test;
    a *test2 = new a();
    delete test;
    return 0;
}

Если я не создаю промежуточный экземпляр этого класса между удалениями, 2 обращения к свободным в той же памяти происходят, как и ожидалось:

*** Error in `./a.out': double free or corruption (fasttop): 0x000000000111a010 ***

Чтобы ответить на ваши вопросы напрямую:

Что может случиться худшее :

Теоретически ваша программа вызывает что-то фатальное. В некоторых крайних случаях он может даже попытаться стереть ваш жесткий диск. Вероятность зависит от того, что на самом деле представляет ваша программа (драйвер ядра? Программа пространства пользователя?).

На практике, скорее всего, это просто сбой с segfault. Но может случиться что-то худшее.

Будет ли компилятор выдавать ошибку ?

Не должно.

1 голос
/ 23 февраля 2015

Нет, дважды удалять один и тот же указатель небезопасно. Это неопределенное поведение в соответствии со стандартом C ++.

Из C ++ FAQ: посетите эту ссылку

Безопасно ли дважды удалять один и тот же указатель?
Нет! (Предполагая, что вы не получили этот указатель от нового между ними.)

Например, следующая катастрофа:

class Foo { /*...*/ };
void yourCode()
{
  Foo* p = new Foo();
  delete p;
  delete p;  // DISASTER!
  // ...
}

Эта вторая строка удаления p может сделать для вас действительно плохие вещи. Это может, в зависимости от фазы луны, повредить вашу кучу, вывести из строя вашу программу, внести произвольные и странные изменения в объекты, которые уже находятся в куче, и т. Д. К сожалению, эти симптомы могут появляться и исчезать случайно. Согласно закону Мерфи, вы будете поражены сильнее всего в самый неподходящий момент (когда клиент ищет, когда транзакцию с высокой стоимостью пытается опубликовать и т. Д.). Примечание: некоторые системы времени выполнения защитят вас от некоторых очень простых случаев двойного удаления. В зависимости от деталей, вы можете быть в порядке, если вы работаете в одной из этих систем, и если никто не развертывает ваш код в другой системе, которая обрабатывает вещи по-другому, и если вы удаляете что-то, у кого нет деструктора, и если вы не делаете ничего существенного между двумя удалениями, и если никто никогда не изменяет ваш код, чтобы сделать что-то значимое между этими двумя удалениями, и если ваш планировщик потоков (над которым вы, вероятно, не имеете никакого контроля!) не переставляет потоки между два удаляет и если, и если, и если. Итак, вернемся к Мерфи: так как это может пойти не так, как надо, так и будет в самый неподходящий момент. Отсутствие сбоя не доказывает отсутствие ошибки; это просто не в состоянии доказать наличие ошибки. Поверь мне: двойное удаление - это плохо, плохо, плохо. Просто сказать нет.

...