Разыменовывание нулевого указателя приводит к неопределённому поведению

Ненароком я породил большую дискуссию, касающуюся того, допустимо ли использовать в Си/Си++ выражение &P->m_foo, если P является нулевым указателем. Программисты разделились на два лагеря. Одни уверенно доказывали, что так писать нельзя, другие столь же уверенно утверждали, что можно. Приводились различные аргументы и ссылки. И я понял, что нужно внести окончательную ясность в этот вопрос. Для этого я обратился к экспертам Microsoft MVP и разработчикам Visual C++, общающимся через закрытый список рассылки. Они помогли подготовить эту статью, и я представляю её всем желающим. Для нетерпеливых: этот код не корректен.
Напомню историю обсуждений
Все началось со статьи о проверке ядра Linux с помощью анализатора PVS-Studio. Но сама проверка ядра тут ни причём. Дело в том, что в статье я привёл следующий фрагмент из кода Linux:
static int podhd_try_init(struct usb_interface *interface, struct usb_line6_podhd *podhd) < int err; struct usb_line6 *line6 = &podhd->line6; if ((interface == NULL) || (podhd == NULL)) return -ENODEV; . >
Я назвал этот код опасным, так как посчитал, что здесь имеет место неопределённое поведение.
По этому поводу я получил много возражений от читателей и даже одно время был готов поддаться на их убедительные речи в письмах и комментариях. Например, в качестве доказательства корректности кода приводили устройство макроса offsetof, который часто реализован так:
#define offsetof(st, m) ((size_t)(&((st *)0)->m))
Здесь имеет место разыменование нулевого указателя, но код успешно работает. Были и другие письма с рассуждениями того, что раз нет доступа по нулевому указателю, то нет и проблемы.
Хотя я и доверчивый, но стараюсь проверять информацию. Я начал разбираться с этой темой и в результате написал небольшую статью: «Размышления над разыменованием нулевого указателя».
По всему выходило, что я был прав. Так писать нельзя. Однако я не смог окончательно обосновать свою позицию и привести нужные ссылки на стандарт.
После статьи вновь последовали письма с возражениями, и я понял, что надо разобраться с данной темой окончательно. Я обратился с вопросом к экспертам, чтобы узнать их мнение. Эта статья является их обобщенным ответом.
О языке Си
Выражение ‘&podhd->line6’ является неопределенным поведением в языке C в том случае, если ‘podhd’ — нулевой указатель.
Вот что говорится про оператор взятия адреса ‘&’ в стандарте C99 (Раздел 6.5.3.2 «Операторы взятия адреса и разыменовывания»):
Операнд унарного оператора & должен быть либо указателем функции, либо результатом оператора [] или унарного оператора *, либо lvalue-выражением, указывающим на объект, который не является битовым полем и не содержит в объявлении спецификатора регистрового класса памяти.
Выражение ‘podhd->line6’ однозначно не является указателем функции, результатом оператора [] или *. Это как раз lvalue-выражение. Однако, когда указатель ‘podhd’ равен нулю, выражение не указывает на объект, поскольку в Разделе 6.3.2.3 «Указатели» сказано следующее:
Если константа нулевого указателя приводится к типу указателей, то результирующий указатель, называемый нулевым, гарантированно будет не равен указателю на любой объект или функцию.
Если «lvalue-выражение не указывает на объект при своем вычислении, возникает неопределенное поведение» (Стандарт C99, Раздел 6.3.2.1 «Lvalue-выражения, массивы и указатели функций»):
lvalue — это выражение объектного типа или неполного типа, отличного от void; если lvalue-выражение не указывает на объект при своем вычислении, возникает неопределенное поведение.
Когда оператор -> был применен к указателю, его результатом стало lvalue-выражение, для которого не существует объекта, и в результате мы имеем дело с неопределенным поведением.
О языке Си++
В языке С++ всё обстоит точно также. Выражение ‘&podhd->line6’ является неопределенным поведением в языке C++ в том случае, если ‘podhd’ — нулевой указатель.
С толку немного сбивает дискуссия на WG21 (232. Is indirection through a null pointer undefined behavior?), на которую я ссылался в предыдущей статье. Там настаивают, будто бы такое выражение не является неопределенным поведением. Однако никто так и не нашел никаких правил в стандартах C++, которые разрешали бы использовать «poldh->line6», когда «polhd» — нулевой указатель.
Указатель «polhd» нарушает основное ограничение (Раздел 5.2.5/4, второй пункт в списке) о том, что он должен указывать на объект. Ни один объект в C++ не может иметь адреса nullptr.
Итого
struct usb_line6 *line6 = &podhd->line6;
Этот код является некорректным в языке Си и Си++, если указатель podhd равен 0. Если указатель равен 0, то возникает неопределённое поведение.
То, что программа может работать, является везением. Неопределённое поведение может проявить себя, как угодно. В том числе, программа может работать так, как хотел программист. Это один из частных случаев, но не более того.
Так писать нельзя. Указатель должен быть проверен до разыменования.
Разное в дополнение
- При рассмотрении идиоматической реализации offsetof() следует учитывать, что компилятору разрешено использовать непереносимые приемы для реализации этой функциональности. Тот факт, что в реализации библиотеки в компиляторе используется константа нулевого указателя при реализации offsetof(), вовсе не означает, что в пользовательском коде можно без опаски применять ‘&podhd->line6′ в случае, когда’podhd’ является нулевым указателем.
- GCC может (и делает это) проводить оптимизацию, основываясь на предположении, что никакого неопределенного поведения возникнуть не может, и убрать в данном случае проверки указателей на ноль — поэтому ядро компилируется с набором ключей, указывающих компилятору не делать этого. Например, эксперты в качестве примера ссылаются на статью «What Every C Programmer Should Know About Undefined Behavior #2/3».
- Возможно, вам также будет интересно узнать, что подобным образом нулевой указатель был задействован в эксплойте ядра с помощью TUN/TAP-драйва. Подробности можно посмотреть по ссылке «Fun with NULL pointers». Некоторые могут решить, будто эти два примера имеют мало общего, поскольку во втором случае есть существенное отличие: в баге TUN/TAP-драйвера вместо простого взятия адреса поля структуры, к которому обращался нулевой указатель, это поле было явно взято в качестве значения для инициализации переменной. Однако с точки зрения стандарта C взятие адреса поля с помощью нулевого указателя также является неопределенным поведением.
- А есть ли какая-та ситуация, когда при P == nullptr мы напишем &P->m_foo и всё будет хорошо? Да, например это может быть аргументом оператора sizeof: sizeof(&P->m_foo).
Благодарности
- Майкл Бёрр — горячий поклонник языка C/C++ и специалист по системному и встроенному ПО, в том числе службам Windows, работе с сетями и драйверам устройств. Активно участвует в жизни сообщества StackOverflow, отвечая на вопросы программистов по C и C++ (а иногда и на некоторые простые вопросы по C#). Имеет 6 наград Microsoft MVP в номинации Visual C++.
- Билли О’Нил — разработчик ПО на C++ (преимущественно) и активный участник сообщества StackOverflow. Является инженером-разработчиком ПО в подразделении по совершенствованию систем безопасности Microsoft (Trustworthy Computing Team). До этого работал в нескольких компаниях, занимающихся безопасностью ПО, в числе которых — Malware Bytes и PreEmptive Solutions.
- Джованни Диканио — программист, специализирующийся на разработке ОС Windows. Автор статей для программистов по C++, OpenGL и другим темам в ряде итальянских компьютерных журналов. Также писал код для некоторых открытых проектов. Джованни помогает коллегам, давая советы по решению программистских проблем, связанных с C и C++, на форумах Microsoft MSDN, а с некоторых пор — и на StackOverflow. Имеет 8 наград Microsoft MVP в номинации Visual C++.
- Габриэль Дус Рейс — главный инженер-разработчик ПО Microsoft. Также является исследователем и долгосрочным участником C++-сообщества. Одно из направлений его научных интересов и исследований — средства разработки надежного ПО. До того, как прийти в Microsoft, работал старшим преподавателем в Техасском Университете A&M (Texas A&M University). В 2012 году Доктор Дус Рейс был отмечен премией Национального Научного Фонда (National Science Foundation CAREER Award) за проведенное им исследование компиляторов надежного ПО в области вычислительной математики и за образовательную деятельность. Является членом комитета по стандартизации языка C++.
Дополнительные ссылки
- Wikipedia. Неопределённое поведение.
- A Guide to Undefined Behavior in C and C++. Part 1, 2, 3.
- Wikipedia. offsetof.
- LLVM Blog. What Every C Programmer Should Know About Undefined Behavior #2/3.
- LWN. Fun with NULL pointers. Part 1, 2.
c++ Null vs nullptr
Добрый день, вчера пытался разобраться в чем преимущество (конкретно, а не в виде абстрактных понятий типа ООП и прочая эзотерика) nullptr против NULL. Ничего внятного (для простых инженеров, а не всяких там страуструпов) не нашел, кроме синтетического примера
void foo(int *) < coutvoid foo(int) < coutfoo(NULL); foo(nullptr);
foo(int) foo(int *)
Но этого что-то мало и вроде я такое раз в год вызываю, может кто-нибудь покажет доступные, бытовые примеры когда nullptr лучше и удобней чем NULL.
da17 ★
28.01.19 13:25:53 MSK

Но этого что-то мало и вроде я такое раз в год вызываю
Так можно про многие правила из стандарта сказать, типа я это почти не использую, а потому не нужно.
seiken ★★★★★
( 28.01.19 13:44:42 MSK )
Ответ на: комментарий от seiken 28.01.19 13:44:42 MSK
Так из-за этого и спрашиваю, где это используется?
da17 ★
( 28.01.19 13:51:37 MSK ) автор топика
у меня твой пример не компилируется.
error: call of overloaded ‘foo(NULL)’ is ambiguous
conalex ★★★
( 28.01.19 13:57:09 MSK )
Ответ на: комментарий от conalex 28.01.19 13:57:09 MSK
MSVC 2013 собирал, там норм прошло
da17 ★
( 28.01.19 14:16:43 MSK ) автор топика
Ответ на: комментарий от da17 28.01.19 13:51:37 MSK

NULL это просто 0, ещё и определённый макросом, т.е. компилятор получит просто 0 в этом куске кода, некоторые конструкции аля auto не поймут что именно ты хотел использовать число или указатель
sparks ★★★
( 28.01.19 14:33:13 MSK )

Effective Modern C++ Майерса.
Item 8: Prefer nullptr to 0 and NULL.
Показано развитие приведённого тобою примера для случая когда такие перегрузки за шаблонами скрыты.
mkam ★
( 28.01.19 14:36:12 MSK )

nullptr нельзя по ошибке присвоить переменной, не являющейся указателем, с NULL такое скомпилируется и может потом привести к многочасовой отладке
annulen ★★★★★
( 28.01.19 14:40:07 MSK )
Ответ на: комментарий от annulen 28.01.19 14:40:07 MSK
da17 ★
( 28.01.19 15:08:26 MSK ) автор топика
На будущее: когда тебе говорят что NULL это легаси и нужно пользоваться nullptr, тебе нужно просто это сделать а не задаваться бесполезными вопросами.
Если же тебе неймётся, все ответы есть на cppreference. NULL - это implementation-defined макрос который может в зависимости от реализации раскрываться в 0 или nullptr, поэтому вопрос сводится к тому почему нельзя использовать 0 (или, что эквивалентно, макрос который может в него раскрыться). Потому что это нарушает типобезопасность - 0 можно передать в функцию принимающую int, или сравнить с int’ом. Если ты сравниваешь NULL с int’ом, или передаёшь в качестве int’а - это явная ошибка, но в этом случае компилятор её не отловит. У nullptr таких проблем нет. Кроме того, он может участвовать в перегрузках (https://en.cppreference.com/w/cpp/types/nullptr_t).
slovazap ★★★★★
( 28.01.19 15:09:25 MSK )
Ответ на: комментарий от da17 28.01.19 15:08:26 MSK

void f(int a); f(nullptr); // error
annulen ★★★★★
( 28.01.19 15:14:36 MSK )
Ответ на: комментарий от slovazap 28.01.19 15:09:25 MSK
сравниваю NULL с int
#include #include class Keker< >; int main()
Компилятор ругается In function 'int main()': 12:16: error: ISO C++ forbids comparison between pointer and integer [-fpermissive]
da17 ★
( 28.01.19 15:16:36 MSK ) автор топика
Дальше не читал
Gvidon ★★★★
( 28.01.19 15:19:10 MSK )
Ответ на: комментарий от Gvidon 28.01.19 15:19:10 MSK
Я прочитал пример https://en.cppreference.com/w/cpp/types/nullptr_t т.е. если я все правильно понял, nullptr помогает явно указать, что должна вызываться ф-ия принимающая указатель и если у программиста функция, принимающая foo(int), а он где-то в дебрях кода передает ей нулевой указатель путем foo(NULL), то возможно он ошибся и nullptr от таких косяков уберегает, т.е. если даже программист забудет, чего принимает ф-ия, компилятор его поправит при nullptr, чего не будет при NULL?
da17 ★
( 28.01.19 15:30:21 MSK ) автор топика
Ответ на: комментарий от Gvidon 28.01.19 15:19:10 MSK
да я пошутил, а то иногда напишут вместо пояснения «НАРУШАЕТ ПРИНЦИПЫ ООП» и гадай, что оно там нарушает
da17 ★
( 28.01.19 15:32:18 MSK ) автор топика
История примерно такая:
1) В С++ запретили неявное приведение void* к любому другому указателю, что в С разрешено. Внезапно, отломался код
int* foo = NULL;
т.к. NULL всегда определялся как (void*)0.
2) Не унывая, в С++ решили эту проблему переиспользованием константы 0, что привело к вот такой вот радости (примерно):
#ifdef __cplusplus #define NULL 0 #else #define NULL (void*)0 #endif
Что, в свою очередь, начало весело стрелять при перегрузках и в шаблонах.
3) Компиляторы от такой весёлой жизни начали заводить свои нестандартные «нулевые указатели», чтобы с этим адом хоть как-то бороться. Например __null в gcc.
4) Пункт 3) узаконили в стандарте как nullptr
Всё это время сишники смотрят на плюсовиков как на стадо дебилов, пишут свой родной NULL и в ус не дуют.
Gvidon ★★★★
( 28.01.19 15:59:55 MSK )
Ответ на: комментарий от da17 28.01.19 15:16:36 MSK
И что вы хотели сказать этим примером? К NULL vs. nullptr он вообще никакого отношения не имеет.
slovazap ★★★★★
( 28.01.19 16:32:37 MSK )
В чистом Си у тебя ведь перегрузки функций нет? Вот там это в ногу не выстрелит. А в костылях выстрелит и будет неприятно.
Вот и пиши nullptr , чтобы избежать двусмысленности.
Deleted
( 28.01.19 16:41:55 MSK )
Ответ на: комментарий от da17 28.01.19 15:08:26 MSK
Например такой: #include
int main() < char str[] = "hello"; char* p = str; /* что-то делаем со строкой */ p = 0; >
То есть мы работаем со строкой, теперь захотели ограничить строку на текущем символе, и вместо
*p = '\0';
написали просто
p = 0;
Какой-то пример с массивом строк и присваиванием нуля, вместо '\0', был в одном из разборов PVS-studio какого-то опенсурс проекта, так что несмотря на то что пример искусственный, что-то подобное есть и в реальных проектах.
Но в gcc/clang есть полезное предупреждение
-Wzero-as-null-pointer-constant
NULL == nullptr в C++11?
не совcем. NULL большинством компиляторов с легкостью трактуется как 0 (на самом деле это обычно define). Но нет 100 гарантии, что это так. А вот nullptr - это известная для компилятора константа и компилятор знает, что это такое.
6 дек 2013 в 13:29
@KoVadim, можно сказать, что надежнее будет инициализировать указатели nullptr, для дальнейшей проверки на корректность, нежели NULL?
6 дек 2013 в 13:58
nullptr как раз специально ввели, что бы решить часть неоднозначных ситуаций с NULL. Это все лучше утрясется в голове, если понимать, то компилятор не видет NULL, он видит обычно вместо него 0 (препроцессор постарался). А вот nullptr виден. И компилятор может сделать анализ.
6 дек 2013 в 14:43
@SoloMio, не совсем. Везде где идет речь про указатели НУЖНО использовать null_ptr. Это гарантия того, что компилятор попробует застраховать программиста от ошибки.
Нулевые указатели (null и nullptr) в C++. Учимся ходить по граблям изящно

В этом материале для новичков мы рассуждаем про обнаружение в коде C++ распространенного дефекта «разыменование нулевого указателя», попутно объясняя его скрытую коварность.
Програмний курс від robotdreams: С++ для GameDev.
Розробка ігор на високому рівні.

1. Разыменование нулевого указателя
Сегодня рассмотрим причину дефекта в коде С++, который получается, если программа обращается по некорректному указателю к какому-то участку памяти. Такое обращение ведет к неопределенному поведению программы, что приводит в большинстве случаев к аварийному завершению. Данный дефект получил название разыменование нулевого указателя ( CWE-476 ). Мы поговорим о том, что такое NULL и nullptr и для чего они нужны.
По сути, это почти одинаковые вещи, но есть нюансы.
Освітній курс від robotdreams: Аналітик даних.
Перетворюйте дані на рішення.
#include #include using namespace std; /* * Работа с динамической памятью. Нулевые указатели */ void main()
Язык С++ не имеет автоматического сборщика мусора, как, например, в Java или C#. Если мы выделяем область под данные, то никто кроме нас не позаботится о том, чтобы область памяти была очищена. Если в памяти находится одно число, это не является проблемой.
Однако, если, например, программа работает в цикле, используя миллион итераций, и в каждой итерации запрашиваются в операционной системе новое место под данные, то тут возможны проблемы. На каком-то этапе место в памяти заканчивается и наша программа аварийно завершает работу. Поэтому, чтобы избежать подобной ситуации, следует использовать delete для очистки неиспользуемых данных, указав указатель на область данных.
Если вы начинающий программист, возьмите себе за правило, каждый раз, как только вы используете оператор new, выделяя под что-то память, тут же (в этой же функции) писать deletе , чтобы потом не забыть сделать это. Это избавит вас от потенциальных проблем с утечкой памяти.
Итак, в нашем коде (пример выше) с помощью оператора new мы выделили место для нашей оперативной памяти. После очистки места, которое мы выделяли под данные в динамической части оперативной памяти, сами данные исчезают. В следствии действия оператора delete данные уничтожаются и система может выделять память, которую мы уже не используем, для любых других своих нужд.
Однако, у нас остается проблема! В нашем указателе *pa все еще сохранен адрес на тот участок памяти, где у нас лежали данные и, в принципе, нам никто не запрещает туда обращаться.

Мы можем туда что-нибудь записать или получить данные, которые там находятся. А можем что-нибудь повредить либо получить некорректные данные, далее невольно начать с ними работать если у нас есть подобная ошибка в логике. В нашем случае, например, мы можем по указателю получить вот такое число — 842150451 .
Короче говоря, надеюсь, вы поняли эту тонкость, что после деинициализации ранее занятой памяти наш старый указатель может продолжать указывать в удаленное место памяти, что потенциально может привести к краху программы.
Ефективний курс від robotdreams: Blockchain-розробник.
Революційні рішення в технологіях.
2. Нулевое значение и нулевые указатели
Если мы ведем какие-то расчеты и вдруг получаем некоторое случайное число (не имеющее отношение к расчетам), мы можем даже не узнать, что что-то пошло не так. Неплохо было бы определиться и отметить указатель каким-то таким образом, чтобы он никуда не указывал (чтобы однозначно исключить патовую ситуацию описанную в примере выше).
Для этого и существуют NULL и nullptr . Обратите внимание, что если у нас сейчас вызывается оператор delete на нашем указателе (мы очищаем находящуюся по нему память), то оттуда данные теряются.
Если мы опять принудительно выведем на консоль эти данные (из освобожденного участка памяти), то в принципе, у нас может случиться чудо — мы увидим в консоли тот «мусор», который сейчас в памяти (куда указывает наш указатель, после того как мы его почистили).
void main()
Но, если мы еще раз возьмем и вызовем оператор delete pa , то все закончится очень плохо — мы увидим на экране сообщение об ошибке. Она говорит о том, что возникла проблема при работе с кучей, то есть с динамической памятью.
void main()
Для того, чтобы избежать такой проблемы мы можем использовать NULL .

3. NULL и nullptr
В таких языках программирования как Java или C#, NULL является отдельным типом данных и там ситуация несколько иная. В случае С++ мы имеем дело с NULL и nullptr .
nullptr — это более новая разработка, добавленная в С++ 11, и она уже работает аналогично тому как это реализовано в Java или C#. nullptr это отдельный тип данных, с которым компилятор ничего спутать не может. Что же касается NULL , то это просто эквивалент записи 0 (ноль).
Если мы напишем pa = 0 , то это значит, что наш указатель pa теперь не будет хранить адрес в памяти, а будет нулевым указателем, указывать на ноль. Вместо pa = 0 , мы можем записать pa = NULL — эти записи абсолютно равнозначны. Все дело в том, что NULL — это просто макрос.
Если мы наведем на него мышку, поставим курсор и нажимаем f12 , то увидим #define NULL 0 .
Спеціалізований курс від robotdreams: Frontend Engineer.
Створюйте вражаючий веб.

Строка pa = NULL говорит указателю, который до момента выполнения данной строки (где мы уже почистили память) указывает на определенный адрес оперативной памяти, чтобы он этот адрес забыл, чтобы мы к этому адресу впоследствии случайно не обратились. После того как мы присвоили NULL , у нас одни нули, т.е. нулевой указатель.
Если после такой операции мы попробуем еще раз сделать delete pa , то у нас все пройдет без проблем. Оператор delete посмотрит на то, что указатель указывает на NULL и не будет пытаться там что-то очистить, поэтому ошибку не получим. Теперь также мы явно можем проверять наш указатель на NULL , то есть на то, содержит ли он какой-то адрес или нет.
Если сейчас попробовать обратиться через cout , то в консоль будет выведен наш адрес — одни нули.

Добавим проверку if pa != 0 или if pa != NULL с возможностью выводить наш адрес указателя. В данном случае адрес не вывелся, поскольку указатель указывает на NULL . А раз он указывает на NULL , то он в принципе ничего не может хранить.
Таким образом мы перестраховываемся от того, чтобы получить или случайно записать некорректные данные.
void main() < int *pa = new int; *pa = 10; cout delete pa; >
Если мы уберем запись pa = NULL , то не сможем знать, куда указывает указатель, мы не можем перебрать все возможные адреса и знать что там лежит. Поэтому мы получим вывод нашего адреса и ошибку.
4. Тип данных nullptr
Возникает вопрос — для чего нужен отдельный тип данных nullptr? В принципе, мы можем использовать и NULL . В данном случае работать это будет точно так же.
void main() < int *pa = new int; *pa = 10; cout delete pa; >
Это уже не просто макрос и не просто нолик, а целочисленный тип Int . Это уже — отдельный тип данных. Мы его присваиваем и, на первый взгляд, разницы никакой нет. Однако для компилятора разница есть, он никогда не перепутает указатель nullptr с целочисленным типом данных.
К примеру, если у вас будет какая-то функция, она будет перегружена для типа Int и для указателя. И вы захотите передать в вашу функцию указатель с целочисленным нулем pa = 0 :
void main() < int *pa = new int; *pa = 10; cout delete pa; >
Ваша функция принимает либо указательную Int , либо просто целочисленный тип Int . В таком случае у компилятора могут быть проблемы какую функцию вызвать — с реализацией получается неоднозначность.
В С++ 11 nullptr это отдельный тип данных и компилятор никогда не перепутает его с обычным int . Поэтому в случаях когда вы будете работать с указателями, рекомендуется использовать именно его.
Если вы встретите где-то старый код, вы можете увидеть запись с присвоением нуля pa = NULL; (pa = 0;) . Теперь вы будете знать, что это такое и какие могут быть проблемы. Справедливости ради нужно сказать, что на самом деле проблемы возникают редко, но чтобы исключить их вообще, лучше использовать nullptr . Это хоть и редкий тип проблем, но очень коварный и трудно вычислимый.
Также стоит обратить внимание на еще один тип ошибок. Если вам нужно очистить динамическую память, в которой выделено место под ваши данные, то обязательно сначала нужно вызвать delete , ну, а затем, если нужно затереть адрес — присваивать нашему указателю nullptr .
void main() < int *pa = new int; *pa = 10; cout delete pa; >
Если вы сделаете наоборот, то есть сначала присвоите указателю nullptr , а затем присвоите указателю delete , то такое ваше действие приведет к утечке памяти.
void main() < int *pa = new int; *pa = 10; cout delete pa; >
Представьте себе, что мы имеем дело с программой, где указатель ссылается на определенные данные в оперативной памяти.


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

Если в результате таких утечек памяти ваша программа упадет (если подобных утечек будет много), либо просто аварийно завершится, операционная система сама почистит память. Поэтому всегда нужно сначала удалять данные из динамической памяти, а затем присваивать nullptr , но ни коем случае не наоборот!
Заключение
Надеемся, что наш материал поможет вам избежать частых проблем при работе с памятью в C++. В заключение рекомендуем посмотреть видео, в котором рассказывается про указатели в С++
а также про работу с динамической памятью при работе с массивами