Что Visual Studio делает с удаленным указателем и почему?

130

В книге на C ++, которую я читал, говорится, что когда указатель удаляется с помощью deleteоператора, память в том месте, на которое он указывает, «освобождается» и может быть перезаписана. В нем также указано, что указатель будет продолжать указывать на то же место, пока он не будет переназначен или установлен на NULL.

Однако в Visual Studio 2012; похоже, что это не так!

Пример:

#include <iostream>

using namespace std;

int main()
{
    int* ptr = new int;
    cout << "ptr = " << ptr << endl;
    delete ptr;
    cout << "ptr = " << ptr << endl;

    system("pause");

    return 0;
}

Когда я компилирую и запускаю эту программу, я получаю следующий результат:

ptr = 0050BC10
ptr = 00008123
Press any key to continue....

Очевидно, что адрес, на который указывает указатель, изменяется при вызове удаления!

Почему это происходит? Связано ли это конкретно с Visual Studio?

И если delete в любом случае может изменить адрес, на который он указывает, почему бы delete автоматически не установить указатель NULLвместо некоторого случайного адреса?

tjwrona1992
источник
4
Удаление указателя не означает, что он будет установлен в NULL, вы должны позаботиться об этом.
Мэтт
11
Я знаю это, но в книге, которую я читаю, конкретно говорится, что она по-прежнему будет содержать тот же адрес, на который указывал перед удалением, но содержимое этого адреса может быть перезаписано.
tjwrona1992
6
@ tjwrona1992, да, потому что обычно так и происходит. В книге просто перечислены наиболее вероятные исходы, а не жесткие правила.
SergeyA
5
@ tjwrona1992 Книга по C ++, которую я читал, и название книги ...?
PaulMcKenzie
4
@ tjwrona1992: Это может быть удивительно, но все это использование неверного значения указателя, которое является неопределенным поведением, а не только разыменование. «Проверка того, куда он указывает», ИСПОЛЬЗУЕТ значение недопустимым способом.
Бен Фойгт

Ответы:

175

Я заметил, что адрес, хранящийся в, ptrвсегда перезаписывался 00008123...

Это казалось странным, поэтому я немного покопался и нашел это сообщение в блоге Microsoft, содержащее раздел, посвященный «Автоматическая очистка указателя при удалении объектов C ++».

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

По этой причине мы выбрали 0x8123 в качестве значения очистки - с точки зрения операционной системы это находится на той же странице памяти, что и нулевой адрес (NULL), но нарушение прав доступа по адресу 0x8123 будет лучше выделяться разработчиком как требующее более пристального внимания. ,

Он не только объясняет, что Visual Studio делает с указателем после его удаления, но и объясняет, почему они решили НЕ устанавливать его NULLавтоматически!


Эта «функция» включена как часть настройки «Проверки SDL». Чтобы включить / отключить его, перейдите в: ПРОЕКТ -> Свойства -> Свойства конфигурации -> C / C ++ -> Общие -> Проверки SDL

Чтобы подтвердить это:

Изменение этого параметра и повторный запуск того же кода дает следующий результат:

ptr = 007CBC10
ptr = 007CBC10

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


ОБНОВИТЬ:

Проработав еще 5 лет в программировании на C ++, я понял, что вся эта проблема в основном спорный. Если вы программист на C ++ и все еще используете newи deleteуправляете необработанными указателями вместо использования интеллектуальных указателей (которые позволяют обойти всю эту проблему), вы можете подумать об изменении карьеры, чтобы стать программистом на C. ;)

tjwrona1992
источник
12
Хорошая находка. Я бы хотел, чтобы MS лучше документировала такое поведение отладки. Например, было бы неплохо узнать, в какой версии компилятора это реализовано и какие параметры включают / отключают это поведение.
Майкл Берр,
5
«с точки зрения операционной системы это находится на той же странице памяти, что и нулевой адрес» - да? Разве стандартный (без учета больших страниц) размер страницы на x86 по-прежнему составляет 4 КБ как для Windows, так и для Linux? Хотя я смутно помню кое-что о первых 64 Кб адресного пространства в блоге Раймонда Чена, так что на практике я беру тот же результат,
Voo
12
Windows @Voo резервирует первые (и последние) 64 КБ ОЗУ как мертвое пространство для захвата. 0x8123 падает в там красиво
трещотки урод
7
На самом деле, это не поощряет вредные привычки и не позволяет вам пропустить установку указателя на NULL - вот и вся причина, по которой они используют 0x8123вместо 0. Указатель по-прежнему недействителен, но вызывает исключение при попытке разыменовать его (хорошо), и он не проходит проверки на NULL (также хорошо, потому что не делать этого - ошибка). Где место вредным привычкам? Это действительно то, что помогает вам отлаживать.
Луаан,
3
Ну, он не может установить оба (все) из них, так что это второй лучший вариант. Если вам это не нравится, просто отключите проверки SDL - я считаю их весьма полезными, особенно при отладке чужого кода.
Луаан,
30

Вы видите побочные эффекты /sdlопции компиляции. Включенный по умолчанию для проектов VS2015, он позволяет выполнять дополнительные проверки безопасности помимо тех, которые предоставляет / gs. Используйте «Проект»> «Свойства»> «C / C ++»> «Общие»> «Проверки SDL», чтобы изменить его.

Цитата из статьи MSDN :

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

Имейте в виду, что установка удаленных указателей на NULL - плохая практика при использовании MSVC. Это сводит на нет помощь, которую вы получаете как от Debug Heap, так и от этого параметра / sdl, вы больше не можете обнаруживать недопустимые вызовы free / delete в своей программе.

Ганс Пассант
источник
1
Подтверждено. После отключения этой функции указатель больше не перенаправляется. Благодарим за предоставленную фактическую настройку, которая ее изменяет!
tjwrona1992
Ганс, по-прежнему считается плохой практикой устанавливать удаленные указатели на NULL в случае, когда у вас есть два указателя, указывающие на одно и то же место? Когда вы deleteодин, Visual Studio оставит второй указатель, указывающий на его исходное местоположение, которое теперь недействительно.
tjwrona1992
1
Мне довольно непонятно, какой магии вы ожидаете, установив указатель на NULL. Этот другой указатель не так, поэтому он ничего не решает, вам все равно нужен отладчик, чтобы найти ошибку.
Ханс Пассан
3
VS не очищает указатели. Это их развращает. Так что ваша программа все равно выйдет из строя, когда вы их используете. Распределитель отладки делает то же самое с памятью кучи. Большая проблема с NULL в том, что он недостаточно поврежден. В противном случае обычная стратегия Google "0xdeadbeef".
Ханс Пассан
1
Установка указателя в NULL по-прежнему намного лучше, чем оставлять его указывающим на предыдущий адрес, который теперь недействителен. Попытка записи в указатель NULL не приведет к повреждению данных и, вероятно, приведет к сбою программы. Попытка повторно использовать указатель в этот момент может даже не привести к сбою программы, это может привести к очень непредсказуемым результатам!
tjwrona1992
19

В нем также указано, что указатель будет продолжать указывать на то же место, пока он не будет переназначен или не будет установлен в NULL.

Это определенно вводящая в заблуждение информация.

Очевидно, что адрес, на который указывает указатель, изменяется при вызове удаления!

Почему это происходит? Связано ли это конкретно с Visual Studio?

Это явно находится в пределах языковых спецификаций. ptrнедействителен после вызова delete. Использование ptrпосле того, как это было deleted, является причиной неопределенного поведения. Не делай этого. Среда выполнения может делать все, что захочет, ptrпосле вызова delete.

И если delete все равно может изменить адрес, на который он указывает, почему бы delete автоматически не установить указатель на NULL вместо некоторого случайного адреса ???

Изменение значения указателя на любое старое значение находится в пределах спецификации языка. Что касается изменения его на NULL, я бы сказал, что это было бы плохо. Программа вела бы себя более разумно, если бы значение указателя было установлено в NULL. Однако это скроет проблему. Когда программа скомпилирована с другими настройками оптимизации или перенесена в другую среду, проблема, скорее всего, проявится в самый неподходящий момент.

Р Саху
источник
1
Я не верю, что это отвечает на вопрос ОП.
SergeyA
Не согласен даже после редактирования. Установка его в NULL не скроет проблему - фактически, она обнаружит ее в большем количестве случаев, чем без этого. Есть причина, по которой обычные реализации этого не делают, и причина в другом.
SergeyA
4
@SergeyA, большинство реализаций не делают этого ради эффективности. Однако, если реализация решает установить его, лучше установить значение, отличное от NULL. Это выявит проблемы раньше, чем если бы было установлено значение NULL. Он установлен в NULL, deleteдвойной вызов указателя не вызовет проблемы. Это определенно не хорошо.
Р Саху
Нет, не эффективность - по крайней мере, это не главное.
SergeyA
7
@SergeyA Установка указателя на значение, которое не является, NULLно определенно находится за пределами адресного пространства процесса, обнаружит больше случаев, чем две альтернативы. Если оставить его висящим, это не обязательно вызовет segfault, если он используется после освобождения; установка его на NULLне вызовет segfault, если он deleteснова d.
Blacklight Shining
10
delete ptr;
cout << "ptr = " << ptr << endl;

В общем, даже чтение (как вы это делаете выше, обратите внимание: это отличается от разыменования) значений недопустимых указателей (указатель становится недействительным, например, когда вы deleteэто делаете ) является поведением, определяемым реализацией. Это было представлено в CWG № 1438 . Смотрите также здесь .

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

giorgim
источник
3
Также уместна цитата из [basic.stc.dynamic.deallocation]: «Если аргумент, переданный функции освобождения в стандартной библиотеке, является указателем, который не является значением нулевого указателя, функция освобождения освобождает память, на которую ссылается указатель, делая недействительными все указатели, относящиеся к любому часть освобожденного хранилища »и правило [conv.lval](раздел 4.1), которое гласит, что чтение (преобразование lvalue-> rvalue) любого недопустимого значения указателя является поведением, определяемым реализацией.
Бен Фойгт
Даже UB может быть реализован определенным образом определенным поставщиком, чтобы он был надежным, по крайней мере, для этого компилятора. Если бы Microsoft решила реализовать свою функцию очистки указателя до CWG # 1438, это не сделало бы эту функцию более или менее надежной, и, в частности, просто неправда, что «все может случиться», если эта функция будет включена. , независимо от того, что говорит стандарт.
Кайл Стрэнд
@KyleStrand: Я в основном дал определение UB ( blog.regehr.org/archives/213 ).
giorgim
1
Для большей части сообщества C ++ на SO «все может случиться» воспринимается слишком буквально . Я считаю, что это смешно . Я понимаю определение UB, но я также понимаю, что компиляторы - это просто части программного обеспечения, реализованные реальными людьми, и если эти люди реализуют компилятор так, чтобы он вел себя определенным образом, именно так будет вести себя компилятор , независимо от того, что говорит стандарт. ,
Кайл Стрэнд
1

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

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

На самом деле, чем больше я думаю об этом, тем больше я нахожу, что VS, как обычно, виноват в этом. Что делать, если указатель постоянный? Это все еще изменит это?

Сергея
источник
Да, даже постоянные указатели перенаправляются на этот таинственный 8123!
tjwrona1992
Еще один камень в VS :) Как раз сегодня утром кто-то спросил, почему они должны использовать g ++ вместо VS. Здесь это идет.
SergeyA
7
@SergeyA, но с другой стороны, удаление этого удаленного указателя покажет вам по segfault, что вы пытались удалить удаленный указатель, и он не будет равен NULL. В противном случае произойдет сбой, только если страница также будет освобождена (что очень маловероятно). Терпеть неудачу быстрее; решить раньше.
храповой урод
1
@ratchetfreak «Быстро потерпите неудачу, решите скорее» - очень ценная мантра, но «Быстро потерпите неудачу, уничтожив ключевые доказательства судебной медицины» не является началом такой ценной мантры. В простых случаях это может быть удобно, но в более сложных случаях (тех, в которых нам, как правило, больше всего нужна помощь) стирание ценной информации уменьшает количество доступных мне инструментов для решения проблемы.
Cort Ammon
2
@ tjwrona1992: На мой взгляд, Microsoft поступает правильно. Лучше очистить один указатель, чем ничего не делать. И если это вызывает у вас проблемы при отладке, поставьте точку останова перед неправильным вызовом удаления. Скорее всего, без чего-то подобного вы бы никогда не заметили проблемы. И если у вас есть лучшее решение для поиска этих ошибок, используйте его, и почему вам все равно, что делает Microsoft?
Zan Lynx
0

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

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

В прежние времена MS устанавливала значение на 0xcfffffff.

Карстен
источник