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

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

В чем проблемы со следующим кодом?

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

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

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

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

Ответы

Ответ 1

Установка указателя на 0 (который является "нулем" в стандартном С++, определение 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 для совместного использования или другую реализацию интеллектуального указателя в зависимости от ваших потребностей.

Ответ 2

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

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

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

Ответ 3

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

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

Ответ 4

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

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

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

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

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

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

  • Часто при отладке удобно видеть нулевое значение, а не устаревший указатель.

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

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

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

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

Ответ 5

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

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

Foo * p2 = p;

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

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

Ответ 6

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

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

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

Ответ 7

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

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

Ответ 8

Я немного измени свой вопрос:

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

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

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

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

Ответ 9

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

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

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

Ответ 10

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

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

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

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

Чтобы ребенок во всех ваннах С++ не выбрасывал его.:)

Ответ 11

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

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

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

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

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

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

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

Ответ 12

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

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


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

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

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

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

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

Ответ 13

Да.

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

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

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

#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;
}

изменить

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

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

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

Ответ 14

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

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

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, наверняка расскажу больше), сделают для меня все вопросы типа "Как избежать использования интеллектуальных указателей и сделать работу безопасно с обычными указателями" как устаревшие.

Ответ 15

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

Ответ 16

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

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

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

Ответ 17

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

Ответ 18

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

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

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