Является ли хорошей практикой NULL указатель после его удаления?

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

Каковы проблемы со следующим кодом?

Foo * p = new Foo; // (use p) delete p; p = NULL; 

Это было вызвано ответом и комментариями к другому вопросу. Один комментарий от Нила Баттерворта вызвал несколько авитаций:

Установка указателей на NULL после удаления не является универсальной хорошей практикой в ​​C ++. Бывают моменты, когда это хорошо, и времена, когда это бессмысленно и может скрыть ошибки.

Есть много обстоятельств, где это не помогло бы. Но, по моему опыту, это не повредит. Кто-то просветит меня.

Установка указателя на 0 (который является «нулевым» в стандартном C ++, определение NULL из C несколько отличается) позволяет избежать сбоев при двойных ударах.

Рассмотрим следующее:

 Foo* foo = 0; // Sets the pointer to 0 (C++ NULL) delete foo; // Won't do anything 

В то время как:

 Foo* foo = new Foo(); delete foo; // Deletes the object delete foo; // Undefined behavior 

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

Лучше не иметь двойных ошибок удаления, очевидно, но в зависимости от семантики владения и жизненных циклов объектов это может быть трудно реализовать на практике. Я предпочитаю маскированную двойную ошибку удаления над UB.

Наконец, это побочное решение относительно управления распределением объектов, я предлагаю вам взглянуть на std::unique_ptr для строгого / сингулярного владения, std::shared_ptr для совместного использования или другую реализацию интеллектуального указателя, в зависимости от ваших потребностей.

Установка указателей на NULL после того, как вы удалили то, что она указала, конечно, не может повредить, но часто это небольшая группа помощи по более фундаментальной проблеме: почему вы используете указатель в первую очередь? Я вижу две типичные причины:

  • Вы просто хотели, чтобы что-то было выделено в кучу. В этом случае обертывание его в объект RAII было бы намного безопаснее и чище. Завершите область объекта RAII, когда вам больше не нужен объект. Вот как работает std::vector , и он решает проблему случайного удаления указателей на освобожденную память. Нет указателей.
  • Или, возможно, вам нужна сложная совместная семантика. Указатель, возвращаемый из new может быть не таким, как тот, который вызывается. Несколько объектов могли одновременно использовать объект одновременно. В этом случае предпочтительным был бы общий указатель или что-то подобное.

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

У меня есть лучшая лучшая практика: по возможности прекратите масштаб переменной!

 { Foo* pFoo = new Foo; // use pFoo delete pFoo; } 

Я всегда устанавливаю указатель на NULL (теперь nullptr ) после удаления объекта (ов), на который он указывает.

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

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

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

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

  5. Если вы уже используете RAII, то в вашем коде не так много delete s, поэтому аргумент о том, что дополнительное назначение вызывает беспорядок, не убеждает меня.

  6. При отладке часто бывает удобно видеть нулевое значение, а не устаревший указатель.

  7. Если это вас по-прежнему беспокоит, используйте вместо этого интеллектуальный указатель или ссылку.

Я также устанавливаю другие типы дескрипторов ресурсов для значения no-resource, когда ресурс free’d (который обычно находится только в деструкторе обертки RAII, написанной для инкапсуляции ресурса).

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

Обновление: Microsoft считает, что это хорошая практика для безопасности и рекомендует практику в своих политиках SDL. По-видимому, MSVC ++ 11 автоматически удалит удаленный указатель (во многих случаях), если вы скомпилируете его с опцией / SDL.

Во-первых, существует много существующих вопросов по этой и тесно связанной тематике, например, почему не удаляет указатель на NULL? ,

В вашем коде проблема, о которой идет речь (используйте p). Например, если у вас есть такой код:

 Foo * p2 = p; 

то установка p на NULL выполняется очень мало, так как вы все еще имеете указатель p2, о котором нужно беспокоиться.

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

Если после delete кода появляется больше кода, да. Когда указатель удаляется в конструкторе или в конце метода или функции, Нет.

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

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

Как говорили другие, delete ptr; ptr = 0; delete ptr; ptr = 0; не приведет к тому, что демоны вылетят из вашего носа. Однако это поощряет использование ptr как своего рода флаг. Код усекается delete и устанавливает указатель на NULL . Следующим шагом будет разброс if (arg == NULL) return; через ваш код для защиты от случайного использования указателя NULL . Проблема возникает, когда проверки с NULL становятся вашим основным средством проверки состояния объекта или программы.

Я уверен, что есть запах кода об использовании указателя в качестве флага где-то, но я его не нашел.

Я немного поменяю ваш вопрос:

Вы бы использовали неинициализированный указатель? Вы знаете, что вы не установили NULL или выделили память, на которую он указывает?

Существует два сценария, в которых установка указателя на NULL может быть пропущена:

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

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

Если у вас нет другого ограничения, которое заставляет вас либо установить, либо не установить указатель на NULL после его удаления (одно из таких ограничений было упомянуто Нилом Баттерворт ), тогда мое личное предпочтение – оставить его.

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

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

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

Если это то, что вы на самом деле имеете в виду, лучше сделать это явным в источнике, используя что-то вроде boost :: optional

 optional p (new Foo); // (use p.get(), but must test p for truth first!...) delete p.get(); p = optional(); 

Но если вы действительно хотели, чтобы люди знали, что указатель «испортился», я соглашусь на 100% -ное согласие с теми, кто говорит, что самое лучшее, что нужно сделать, это заставить его выйти из сферы действия. Затем вы используете компилятор, чтобы предотвратить возможность неправильных разыменований во время выполнения.

Это ребенок во всех ваннах C ++, не должен его выбрасывать. 🙂

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

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

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

Хорошо структурированные программы могут быть разработаны с использованием функций C ++, чтобы избежать этих случаев. Вы можете использовать ссылки, или вы можете просто сказать, что «передача / использование пустых или недопустимых аргументов является ошибкой» – подход, который в равной степени применим к контейнерам, таким как интеллектуальные указатели. Повышение последовательного и правильного поведения запрещает этим ошибкам далеко продвигаться.

Оттуда у вас есть только очень ограниченная область действия и контекст, где нулевой указатель может существовать (или разрешен).

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

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

Позвольте мне расширить то, что вы уже задали в своем вопросе.

Вот то, что вы задали в своем вопросе в пулевой форме:


Установка указателей на NULL после удаления не является универсальной хорошей практикой в ​​C ++. Бывают случаи, когда:

  • это хорошая вещь, чтобы сделать
  • и времена, когда это бессмысленно и может скрывать ошибки.

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

Итак, если у вас есть сомнения, просто запустите его.

Сказав это, если вы чувствуете, что вам нужно явно указать какой-либо указатель, то для меня это звучит так, будто вы недостаточно разложили метод и должны посмотреть на метод рефакторинга под названием «Метод извлечения», чтобы разделить метод на отдельный части.

Да.

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

Если вы этого не сделаете, у вас будет некоторая неприятная ошибка с ошибками.

Я всегда использую макрос для удаления:

 #define SAFEDELETE(ptr) { delete(ptr); ptr = NULL; } 

(и аналогичные для массива, free (), выпуская ручки)

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

 static void TreeItem::DeleteSubtree(TreeItem *&rootObject) { if (rootObject == NULL) return; rootObject->UnlinkFromParent(); for (int i = 0; i < numChildren) DeleteSubtree(rootObject->child[i]); delete rootObject; rootObject = NULL; } 

редактировать

Да, эти методы нарушают некоторые правила использования макросов (и да, в наши дни вы, вероятно, могли бы достичь того же результата с помощью шаблонов), но, используя в течение многих лет, я никогда не обращался к мертвой памяти – одной из самых неприятных и сложных наиболее трудоемкий, чтобы отлаживать проблемы, с которыми вы можете столкнуться. На практике на протяжении многих лет они эффективно устраняют class ошибок ошибок из каждой команды, в которую я их представил.

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

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

«Бывают моменты, когда это хорошо, и времена, когда это бессмысленно и может скрыть ошибки»

Я вижу две проблемы: этот простой код:

 delete myObj; myobj = 0 

становится для-вкладыша в многопоточной среде:

 lock(myObjMutex); delete myObj; myobj = 0 unlock(myObjMutex); 

«Наилучшая практика» Дона Нойфельда не всегда применяется. Например, в одном автомобильном проекте нам нужно было установить указатели на 0 даже в деструкторах. Я могу себе представить, что в критическом для безопасности программном обеспечении такие правила нередки. Легче (и мудрее) следовать за ними, чем пытаться убедить команду / проверку кода для каждого использования указателя в коде, что строка, обнуляющая этот указатель, является избыточной.

Другая опасность заключается в использовании этого метода в исключениях – с использованием кода:

 try{ delete myObj; //exception in destructor myObj=0 } catch { //myObj=0; <- possibly resource-leak } if (myObj) // use myObj <--undefined behaviour 

В таком коде вы либо создаете утечку ресурсов, либо откладываете проблему, либо процесс выходит из строя.

Итак, эти две проблемы, возникающие спонтанно через мою голову (Herb Sutter, наверняка расскажу больше), сделают для меня все вопросы типа «Как избежать использования интеллектуальных указателей и сделать работу безопасно с обычными указателями» как устаревшие.

Всегда есть Dangling Point, чтобы беспокоиться.

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

Как говорили многие, гораздо проще просто использовать интеллектуальные указатели.

Изменить: Как сказал Томас Мэтьюз в этом более раннем ответе , если указатель удален в деструкторе, нет необходимости назначать ему NULL, поскольку он больше не будет использоваться, потому что объект уже уничтожен.

Я могу представить, как установить указатель на NULL после удаления его в редких случаях, когда есть законный сценарий повторного использования его в одной функции (или объекте). В противном случае это не имеет смысла – указатель должен указывать на что-то значимое, если оно существует, – период.

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

 shared_ptr p(new Foo); //No more need to call delete 

Он выполняет подсчет ссылок и является streamобезопасным. Вы можете найти его в tr1 (std :: tr1 namespace, #include ) или если ваш компилятор не предоставляет его, получите его от boost.

  • Что я могу использовать вместо оператора стрелки, `->`?
  • size_t vs. uintptr_t
  • Изменение константы с помощью указателя не const
  • Запрещен ли указатель на «внутреннюю структуру»?
  • C Программирование: malloc () внутри другой функции
  • Передача двумерного массива структур
  • Проверка, является ли это нулевым
  • Могу ли я использовать if (указатель) вместо if (pointer! = NULL)?
  • Зачем мне нужно использовать тип ** для указания типа *?
  • Вызывать функцию golang struct дает «не может ссылаться на невыполненное поле или метод»
  • Надежное определение количества элементов в массиве
  • Давайте будем гением компьютера.