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

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

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

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

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

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

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

18 ответов


установка указателя на 0 (который является" null " в стандартном c++, NULL define from 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.

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


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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

Foo * p2 = p;

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

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


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

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

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


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


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

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

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

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

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


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

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

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


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

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

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

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

это ребенок во всей ванне C++, не следует выбрасывать его. :)


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

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

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

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

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

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

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


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

вот что вы вложили в свой вопрос в форме пули:


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

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

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

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

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


да.

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

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

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

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

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

вы также можете написать методы "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;
}

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

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

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

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


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

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

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

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

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


всегда Висячие Указатели о чем беспокоиться.


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

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

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


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


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

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

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