С++ удалить. Он удаляет мои объекты, но я все еще могу получить доступ к данным?

Я написал простую работающую игру тетрис, в которой каждый блок является экземпляром класса singleblock.

class SingleBlock
{
    public:
    SingleBlock(int, int);
    ~SingleBlock();

    int x;
    int y;
    SingleBlock *next;
};

class MultiBlock
{
    public:
    MultiBlock(int, int);

    SingleBlock *c, *d, *e, *f;
};

SingleBlock::SingleBlock(int a, int b)
{
    x = a;
    y = b;
}

SingleBlock::~SingleBlock()
{
    x = 222;
}

MultiBlock::MultiBlock(int a, int b)
{
    c = new SingleBlock (a,b);
    d = c->next = new SingleBlock (a+10,b);
    e = d->next = new SingleBlock (a+20,b);
    f = e->next = new SingleBlock (a+30,b);
}

У меня есть функция, которая сканирует полную строку и просматривает связанный список блоков, удаляя соответствующие блоки и переназначая указатели → next.

SingleBlock *deleteBlock;
SingleBlock *tempBlock;

tempBlock = deleteBlock->next;
delete deleteBlock;

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

Если я распечатаю значения «x» для каждого из удаленных одиночных блоков ПОСЛЕ их удаления, некоторые из них вернут случайный мусор (подтверждая удаление), а некоторые из них вернут 222, говоря мне, что несмотря на то, что был вызван деструктор, данные на самом деле не были удалены из куча. Многие идентичные испытания показывают, что всегда одни и те же конкретные блоки не удаляются должным образом.

Результаты:

Existing Blocks:
Block: 00E927A8
Block: 00E94290
Block: 00E942B0
Block: 00E942D0
Block: 00E942F0
Block: 00E94500
Block: 00E94520
Block: 00E94540
Block: 00E94560
Block: 00E945B0
Block: 00E945D0
Block: 00E945F0
Block: 00E94610
Block: 00E94660
Block: 00E94680
Block: 00E946A0

Deleting Blocks:
Deleting ... 00E942B0, X = 15288000
Deleting ... 00E942D0, X = 15286960
Deleting ... 00E94520, X = 15286992
Deleting ... 00E94540, X = 15270296
Deleting ... 00E94560, X = 222
Deleting ... 00E945D0, X = 15270296
Deleting ... 00E945F0, X = 222
Deleting ... 00E94610, X = 222
Deleting ... 00E94660, X = 15270296
Deleting ... 00E94680, X = 222

Ожидается ли возможность доступа к данным из загробного мира?

Извините, если это немного затянуто.


person Ash    schedule 18.12.2009    source источник
comment
Самая безопасная политика — удалить элемент, когда он больше не используется, и никогда больше к нему не обращаться. Умные указатели могут помочь, когда несколько указателей ссылаются на один и тот же объект в памяти.   -  person Thomas Matthews    schedule 18.12.2009
comment
Если вы можете получить доступ к блокам, вы можете повторно удалить их. Плохо. Не делай этого.   -  person David Thornley    schedule 18.12.2009
comment
Иногда я думаю, что лучшим ключевым словом, чем delete, было бы forget; на самом деле вы не говорите компилятору удалить что-либо, а перестать заботиться об этом (и позволяете кому-то другому делать с i все, что они хотят), вроде как вернуть книгу в библиотеку, а не сжигать ее.   -  person Dan Tao    schedule 18.12.2009
comment
Структура этого кода означает, что класс Multiblock не отвечает за обработку своих собственных членов. Хотя это допустимый С++ (он компилируется и не полагается на неопределенное поведение - игнорируя доступ после удаления, о котором вы здесь говорите), это действительно программа в стиле C. Попробуйте заставить MultiBlock обрабатывать свои собственные элементы, включая операции удаления. Если это не слишком сложно, избегайте раскрытия необработанных указателей вне класса. Эта инкапсуляция, как правило, спасает вас от целого ряда ошибок/утечек памяти в будущем.   -  person Merlyn Morgan-Graham    schedule 18.12.2009
comment
Я согласен с Томасом Мэтьюзом. Используйте интеллектуальные указатели, если можете (ускоренная библиотека shared_pointer - довольно хорошая универсальная). Если вы не хотите использовать зависимость от библиотеки, попробуйте использовать std::list или std::vector вместо того, чтобы вручную кодировать реализацию расширяемого связанного списка/кучи.   -  person Merlyn Morgan-Graham    schedule 18.12.2009
comment
Это пример передовой практики.   -  person lsalamon    schedule 19.12.2009


Ответы (13)


Ожидается ли возможность доступа к данным из загробного мира?

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

person dirkgently    schedule 18.12.2009
comment
Кроме того, хорошо бы добавить следствие этого факта... Если у вас есть конфиденциальные данные, хранящиеся в памяти, следует учитывать, что хорошей практикой является полная перезапись их перед удалением (чтобы предотвратить другие сегменты кода). от доступа к нему). - person Romain; 18.12.2009
comment
Это должно быть обработано до вызова dtor. - person dirkgently; 18.12.2009
comment
@dirkgently: Да, я думаю, что деструктор - правильное место. Вы не хотите делать это слишком рано, и вы не можете сделать это слишком поздно. - person David Thornley; 19.12.2009
comment
@Romain: Нужно просто убедиться, что он не оптимизирован, так как это не наблюдаемое поведение. (Используйте API-функцию, которая гарантированно не будет удалена, не memset.) - person Deduplicator; 26.09.2014

Ожидается ли возможность доступа к данным из загробного мира?

В большинстве случаев да. Вызов удаления не обнуляет память.

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

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

person Martin    schedule 18.12.2009
comment
Однако нет никакой гарантии, что new или malloc не разместят некоторые новые объекты поверх старых. Еще одной бедой может быть системный сборщик мусора. Кроме того, если вашей программе предоставляется память из общесистемного пула памяти, другие программы могут перезаписывать фантомные данные. - person Thomas Matthews; 18.12.2009
comment
Вообще-то, нет. Успешный доступ к удаленной памяти не является ожидаемым поведением, это поведение undefined. Другое выделение может так же легко перезаписать память, которую вы только что освободили. - person Curt Nichols; 18.12.2009
comment
@Thomas Matthews Я не говорю, что это хорошая идея - пытаться получить к нему доступ. @Curt Nichols Это игра словами. В зависимости от того, какой компилятор вы используете, вы можете ожидать, что память не обнуляется сразу при вызове удаления. Хотя, очевидно, вы не можете быть в этом уверены. - person Martin; 18.12.2009

Удалить ничего не удаляет — она просто помечает память как «свободную для повторного использования». Пока какой-либо другой вызов распределения не зарезервирует и не заполнит это пространство, у него будут старые данные. Тем не менее, полагаться на это - большое нет-нет, в основном, если вы что-то удаляете, забудьте об этом.

Одной из практик в этом отношении, которая часто встречается в библиотеках, является функция Delete:

template< class T > void Delete( T*& pointer )
{
    delete pointer;
    pointer = NULL;
}

Это предотвращает случайное обращение к недопустимой памяти.

Обратите внимание, что совершенно нормально вызывать delete NULL;.

person Kornel Kisielewicz    schedule 18.12.2009
comment
Даже если вы не используете макрос, хорошей практикой является установка указателя в NULL сразу после его освобождения. Это хорошая привычка, предотвращающая подобные недоразумения. - person Mark Ransom; 18.12.2009
comment
@Kornel Любая библиотека C ++, которая использовала такой макрос, была бы крайне подозрительной, ИМХО. По крайней мере, это должна быть встроенная функция шаблона. - person ; 18.12.2009
comment
@Mark Установка указателей в NULL после удаления не является универсальной хорошей практикой в ​​C++. Бывают случаи, когда это хорошо, а бывает, что это бессмысленно и может скрыть ошибки. - person ; 18.12.2009
comment
@Ниэль ОГР? И еще несколько, что я видел. Но да, шаблонная функция намного лучше, и я полностью согласен с этой практикой :) - person Kornel Kisielewicz; 18.12.2009
comment
Я ненавижу эту практику. Это очень загромождено, и мех. - person GManNickG; 18.12.2009
comment
Это предотвращает случайное обращение к недопустимой памяти. Это неправда, и это демонстрирует, почему использование этого трюка должно коррелировать с написанием плохого кода. char *ptr = new char; char *ptr2 = ptr; Delete(ptr); *ptr2 = 0;. Я случайно получил доступ к недопустимой памяти. Обнулять ссылку просто запутанно, полагая, что это защищает объект, на который делается ссылка. Также не забывайте, что вам понадобится отдельная версия этой функции для указателей на массивы. - person Steve Jessop; 19.12.2009
comment
@Neil, когда установка указателя на NULL после удаления скрывает ошибку? - person Franci Penov; 19.12.2009
comment
@Franci: когда у вас есть функция, которая должна вызываться только один раз, она удаляет указатель, а затем обнуляет его. Какой-то ошибочный код в другом месте дважды вызывает вашу функцию, и ничего не происходит, несмотря на то, что в этом другом коде есть ошибка двойного освобождения, которая может легко превратиться в ошибку пользователя после освобождения в будущем или при немного других обстоятельствах. - person Steve Jessop; 19.12.2009
comment
Конечно, вы можете проверить значение null перед удалением (и, следовательно, поймать ошибку double-free), но тогда вы потеряете возможность сохранять значение null в этом поле в случаях отсутствия ошибок. - person Steve Jessop; 19.12.2009
comment
Вы не можете полагаться на то, что сможете прочитать данные даже без каких-либо дополнительных выделений, поскольку распределитель может использовать их для любых целей, таких как связанный список для хранения свободных блоков. - person ; 21.12.2009

Это то, что C++ называет неопределенным поведением — вы можете получить доступ к данным, а можете и нет. В любом случае, это неправильно.

person Community    schedule 18.12.2009

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

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

person Fred    schedule 18.12.2009
comment
Если компилятор может определить, что код неизбежно получит доступ (даже просмотр) части доски, которой он не владеет, такое определение освободит компилятор от законов времени и причинности; некоторые компиляторы используют это способами, которые десять лет назад считались бы абсурдными (многие из которых до сих пор абсурдны, ИМХО). Я мог бы понять, говоря, что если две части кода не зависят друг от друга, компилятор может чередовать их обработку любым способом, даже если это приведет к раннему срабатыванию UB, но как только UB становится неизбежным, все правила вылетают из окна. - person supercat; 20.04.2015

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

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

person CB Bailey    schedule 18.12.2009

Система не очищает память при освобождении через delete(). Таким образом, содержимое по-прежнему доступно до тех пор, пока память не будет выделена для повторного использования и перезаписана.

person William Bell    schedule 18.12.2009
comment
Тем не менее, доступ к объекту после его удаления запрещен. Неважно, какое содержимое имеет память. - person walnut; 10.04.2020

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

person Ramónster    schedule 18.12.2009
comment
Будет ли обнулена память или нет, не уточняется. Например. реализация может перезаписать память после удаления в целях отладки или безопасности. - person walnut; 10.04.2020

Да, иногда можно ожидать. В то время как new резервирует место для данных, delete просто делает недействительным указатель, созданный с помощью new, позволяя записывать данные в ранее зарезервированные места; это не обязательно удаляет данные. Однако вам не следует полагаться на такое поведение, потому что данные в этих местах могут измениться в любое время, что может привести к неправильному поведению вашей программы. Вот почему после того, как вы используете delete для указателя (или delete[] для массива, выделенного с помощью new[]), вы должны присвоить ему значение NULL, чтобы вы не могли изменить недопустимый указатель, предполагая, что вы не будете выделять память с помощью new или new[]. перед повторным использованием этого указателя.

person Dustin    schedule 18.12.2009
comment
В стандарте языка C++ нет ничего, что запрещало бы delete стирать память, которая была удалена, или заполнять ее странным значением. Это определяется реализацией. - person Thomas Matthews; 18.12.2009

Он пока не обнулит/изменит память... но в какой-то момент коврик выпадет у вас из-под ног.

Нет, это, конечно, непредсказуемо: это зависит от того, насколько быстро происходит выделение/освобождение памяти.

person jldupont    schedule 18.12.2009
comment
Это может немедленно обнулить память. В стандарте языка нет ничего, что запрещало бы это, и это может иметь смысл для отладки или по соображениям безопасности. В любом случае доступ к объекту после вызова delete является UB. - person walnut; 10.04.2020

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

Я рекомендую, если вы пишете код с new/delete и необработанными указателями (а не std::make_shared() и подобными), выполнять модульные тесты под Valgrind, чтобы хотя бы иметь шанс обнаружить такие ошибки.

person Toby Speight    schedule 31.08.2016

Это приведет к неопределенному поведению, а удаление освобождает память, а не повторно инициализирует ее нулем.

Если вы хотите обнулить его, выполните:

SingleBlock::~SingleBlock()

{    x = y = 0 ; }
person Ashish    schedule 18.12.2009
comment
Это небезопасный способ очистки памяти. Компилятор, вероятно, оптимизировал бы хранилища. И когда деструктор был вызван, вам все равно больше не разрешен доступ к объекту. - person walnut; 10.04.2020

Ну, я тоже давно задавался этим вопросом, и я попытался провести несколько тестов, чтобы лучше понять, что происходит под капотом. Стандартный ответ заключается в том, что после вызова delete не следует ожидать ничего хорошего от доступа к этому участку памяти. Однако этого мне показалось мало. Что на самом деле происходит при вызове delete(ptr)? Вот что я нашел. Я использую g++ в Ubuntu 16.04, так что это может сыграть роль в результатах.

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

Память, освобожденная с помощью delete, по-прежнему кажется выделенной программе, которая сначала выделила ее с помощью new. Я пробовал, и после вызова delete использование памяти не уменьшилось. У меня было программное обеспечение, которое выделяло около 30 МБ списков с помощью вызовов new, а затем освобождало их с последующими вызовами delete. Что произошло, так это то, что, глядя на системный монитор во время работы программы, даже длительный сон после вызовов delete, потребление памяти моей программой было одинаковым. Без снижения! Это означает, что delete не освобождает память для системы.

На самом деле, похоже, что память, выделенная программой, принадлежит ему навсегда! Однако дело в том, что в случае освобождения память может быть снова использована той же программой без дополнительного выделения. Я попытался выделить 15 МБ, освободив их, а затем выделив еще 15 МБ данных, и программа никогда не использовала 30 МБ. Системный монитор всегда показывал около 15 МБ. Что я сделал в отношении предыдущего теста, так это просто изменил порядок, в котором происходили события: половина распределения, половина освобождения, другая половина распределения.

Таким образом, очевидно, память, используемая программой, может увеличиваться, но никогда не уменьшаться. Я подумал, что, возможно, память действительно будет освобождаться для других процессов в критических ситуациях, например, когда памяти больше не будет. В конце концов, какой смысл позволять программе вечно хранить свою память, когда об этом просят другие процессы? Поэтому я снова выделил 30 МБ и освобождая их, запускаю memtester с максимально возможным объемом физической памяти. Я ожидал, что мое программное обеспечение передаст свою память мемтестеру. Но угадайте, этого не произошло!

Я сделал небольшой скринкаст, который показывает, как это работает:

Удалить пример памяти

Если быть на 100% честным, была ситуация, в которой что-то произошло. Когда я попробовал memtester с большим, чем доступная физическая память, в середине процесса освобождения моей программы, память, используемая моей программой, упала примерно до 3 МБ. Однако процесс memtester был автоматически убит, и то, что произошло, было еще более удивительным! Использование памяти моей программой увеличивалось с каждым вызовом удаления! Это было так, как если бы Ubuntu восстанавливала всю свою память после инцидента с memtester.

Взято с сайта http://www.thecrowned.org/c-delete-operator-really-frees-memory

person Ste_95    schedule 06.03.2017