Должен ли кто-нибудь установить указатели на "NULL" после освобождения?
Кажется, есть два аргумента, почему нужно освобождать их указатель на NULL
.
Короткое: вызов free()
второй раз, случайно, не сбой, если он установлен на NULL
.
-
Почти всегда это маскирует логическую ошибку, потому что нет причин называть free()
второй раз. Это безопаснее, если приложение будет аварийно и сможет его исправить.
-
Не гарантируется сбой, потому что иногда новая память выделяется по тому же адресу.
-
Двойное освобождение происходит в основном, когда есть два указателя, указывающих на один и тот же адрес.
Логические ошибки также могут привести к повреждению данных.
Короткие: доступ к свободным указателям может привести к повреждению данных, если malloc()
выделяет память в том же месте, если только освобожденный указатель не установлен на NULL
-
Невозможно гарантировать, что программа выйдет из обращения при указании NULL
, если смещение достаточно велико (someStruct->lastMember
, theArray[someBigNumber]
). Вместо сбоя будет повреждение данных.
-
Установка указателя на NULL
не может решить проблему с другим указателем с тем же значением указателя.
Вопросы
Здесь сообщение против слепого указателя на NULL
после освобождения.
- Какой из них сложнее отлаживать?
- Есть ли возможность поймать оба?
- Насколько вероятно, что такие ошибки приводят к повреждению данных вместо сбоев?
Не стесняйтесь расширять этот вопрос.
Ответы
Ответ 1
Второй способ более важен: повторное использование освобожденного указателя может быть тонкой ошибкой. Ваш код работает правильно, а затем сбой без какой-либо ясной причины, потому что какой-то, казалось бы, несвязанный код написал в памяти, на который указывает повторно используемый указатель.
Мне некогда приходилось работать над действительно багги-программой, которую кто-то написал. Мои инстинкты сказали мне, что многие из ошибок связаны с неряшливыми попытками сохранить указатели после освобождения памяти; Я изменил код, чтобы установить указатели на NULL после освобождения памяти, и bam, началось исключение нулевого указателя. После того как я исправил все исключения нулевого указателя, внезапно код был намного более стабильным.
В моем собственном коде я вызываю только свою собственную функцию, которая является оберткой вокруг free(). Он принимает указатель на указатель и пустит указатель после освобождения памяти. И до того, как он называет бесплатный, он вызывает Assert(p != NULL);
, чтобы он все еще ловил попытки удвоить один и тот же указатель.
Мой код делает и другие вещи, такие как (только в сборке DEBUG), заполняя память очевидным значением сразу после ее выделения, делая то же самое прямо перед вызовом free()
в случае, если есть копия указателя и т.д. Подробности здесь.
EDIT: для запроса, вот пример кода.
void
FreeAnything(void **pp)
{
void *p;
AssertWithMessage(pp != NULL, "need pointer-to-pointer, got null value");
if (!pp)
return;
p = *pp;
AssertWithMessage(p != NULL, "attempt to free a null pointer");
if (!p)
return;
free(p);
*pp = NULL;
}
// FOO is a typedef for a struct type
void
FreeInstanceOfFoo(FOO **pp)
{
FOO *p;
AssertWithMessage(pp != NULL, "need pointer-to-pointer, got null value");
if (!pp)
return;
p = *pp;
AssertWithMessage(p != NULL, "attempt to free a null FOO pointer");
if (!p)
return;
AssertWithMessage(p->signature == FOO_SIG, "bad signature... is this really a FOO instance?");
// free resources held by FOO instance
if (p->storage_buffer)
FreeAnything(&p->storage_buffer);
if (p->other_resource)
FreeAnything(&p->other_resource);
// free FOO instance itself
free(p);
*pp = NULL;
}
Комментарии:
Во второй функции вы можете увидеть, что мне нужно проверить два указателя ресурсов, чтобы узнать, не являются ли они нулевыми, а затем вызовите FreeAnything()
. Это из-за assert()
, который будет жаловаться на нулевой указатель. У меня есть это утверждение, чтобы обнаружить попытку двойного освобождения, но я не думаю, что он на самом деле поймал много ошибок для меня; если вы хотите отказаться от утверждений, вы можете оставить чек и просто позвонить FreeAnything()
. Помимо утверждения, ничего плохого не происходит, когда вы пытаетесь освободить нулевой указатель с помощью FreeAnything()
, потому что он проверяет указатель и просто возвращает, если он уже был нулевым.
Мои имена фактических функций более строгие, но я попытался выбрать самодокументированные имена для этого примера. Кроме того, в моем фактическом коде у меня есть только код для отладки, который заполняет буферы значением 0xDC
перед вызовом free()
, чтобы, если у меня есть дополнительный указатель на ту же самую память (ту, которая не получает нуль) становится действительно очевидным, что данные, на которые он указывает, являются поддельными данными. У меня есть макрос, DEBUG_ONLY()
, который ничего не компилирует в не-отладочной сборке; и макрос FILL()
, который выполняет sizeof()
в структуре. Эти два одинаково хорошо работают: sizeof(FOO)
или sizeof(*pfoo)
. Итак, вот макрос FILL()
:
#define FILL(p, b) \
(memset((p), b, sizeof(*(p)))
Вот пример использования FILL()
для размещения значений 0xDC
перед вызовом:
if (p->storage_buffer)
{
DEBUG_ONLY(FILL(pfoo->storage_buffer, 0xDC);)
FreeAnything(&p->storage_buffer);
}
Пример использования этого:
PFOO pfoo = ConstructNewInstanceOfFoo(arg0, arg1, arg2);
DoSomethingWithFooInstance(pfoo);
FreeInstanceOfFoo(&pfoo);
assert(pfoo == NULL); // FreeInstanceOfFoo() nulled the pointer so this never fires
Ответ 2
Я этого не делаю. Я не особенно помню какие-либо ошибки, с которыми было бы легче справиться, если бы я это сделал. Но это действительно зависит от того, как вы пишете свой код. Есть примерно три ситуации, когда я что-то освобождаю:
- Когда указатель, удерживающий его, собирается выйти из области видимости или является частью объекта, который собирается выйти из области видимости или быть освобожден.
- Когда я заменяю объект на новый (например, при перераспределении).
- Когда я выпускаю объект, который необязательно присутствует.
В третьем случае вы устанавливаете указатель на NULL. Это не специально, потому что вы освобождаете его, потому что все, что угодно, является необязательным, поэтому, конечно, NULL - это особое значение, означающее "у меня его нет".
В первых двух случаях установка указателя на NULL кажется мне занятой без особых целей:
int doSomework() {
char *working_space = malloc(400*1000);
// lots of work
free(working_space);
working_space = NULL; // wtf? In case someone has a reference to my stack?
return result;
}
int doSomework2() {
char * const working_space = malloc(400*1000);
// lots of work
free(working_space);
working_space = NULL; // doesn't even compile, bad luck
return result;
}
void freeTree(node_type *node) {
for (int i = 0; i < node->numchildren; ++i) {
freeTree(node->children[i]);
node->children[i] = NULL; // stop wasting my time with this rubbish
}
free(node->children);
node->children = NULL; // who even still has a pointer to node?
// Should we do node->numchildren = 0 too, to keep
// our non-existent struct in a consistent state?
// After all, numchildren could be big enough
// to make NULL[numchildren-1] dereferencable,
// in which case we won't get our vital crash.
// But if we do set numchildren = 0, then we won't
// catch people iterating over our children after we're freed,
// because they won't ever dereference children.
// Apparently we're doomed. Maybe we should just not use
// objects after they're freed? Seems extreme!
free(node);
}
int replace(type **thing, size_t size) {
type *newthing = copyAndExpand(*thing, size);
if (newthing == NULL) return -1;
free(*thing);
*thing = NULL; // seriously? Always NULL after freeing?
*thing = newthing;
return 0;
}
Это правда, что NULL-ing указатель может сделать его более очевидным, если у вас есть ошибка, где вы пытаетесь разыменовать ее после освобождения. Выделение, вероятно, не наносит немедленного вреда, если вы не указали NULL указатель, но в конечном итоге это неправильно.
Также верно, что NULL-ing указатель скрывает ошибки, когда вы дважды освобождаетесь. Второй бесплатный не наносит немедленного вреда, если вы нанесете указатель NULL, но в конечном итоге он ошибается (потому что он предает то обстоятельство, что жизненные циклы объектов нарушены). Вы можете утверждать, что вещи являются ненулевыми, когда вы их освобождаете, но это приводит к следующему коду для освобождения структуры, которая содержит необязательное значение:
if (thing->cached != NULL) {
assert(thing->cached != NULL);
free(thing->cached);
thing->cached = NULL;
}
free(thing);
Что говорит вам этот код, так это то, что вы зашли слишком далеко. Это должно быть:
free(thing->cached);
free(thing);
Я говорю, NULL указатель, если он должен оставаться пригодным для использования. Если он больше не используется, лучше всего не делать его ложным, представляя собой потенциально значимое значение, например NULL. Если вы хотите спровоцировать ошибку страницы, используйте значение, зависящее от платформы, которое не может быть выведено из обращения, но остальная часть вашего кода не будет рассматриваться как специальное значение "все в порядке и денди":
free(thing->cached);
thing->cached = (void*)(0xFEFEFEFE);
Если вы не можете найти такую константу в своей системе, вы можете выделить нечитаемую и/или незаписываемую страницу и использовать ее адрес.
Ответ 3
Если вы не указали указатель на NULL, у вас мало шансов, что ваше приложение продолжает работать в состоянии undefined и впоследствии выйдет из него в абсолютно несвязанной точке. Затем вы потратите много времени на отладку несуществующей ошибки до того, как узнаете, что это повреждение памяти ранее.
Я бы установил указатель на NULL, потому что шансы выше, если вы нажмете правильное место ошибки раньше, чем если бы вы не установили его в NULL. Логическая ошибка освобождения памяти во второй раз еще должна быть рассмотрена, и ошибка, которую ваше приложение НЕ приводит к сбою в доступе с нулевым указателем с достаточно большим смещением, на мой взгляд вполне академична, но не невозможна.
Заключение: я хотел бы установить указатель на NULL.
Ответ 4
Ответ зависит от (1) размера проекта, (2) ожидаемого срока службы вашего кода, (3) размера команды.
В небольшом проекте с коротким сроком службы вы можете пропустить указатели настроек в NULL и просто отлаживать.
В большом, долгоживущем проекте есть веские причины для установки указателей на NULL:
(1) Оборонительное программирование всегда хорошо. Ваш код может быть в порядке, но начинающий соседний может по-прежнему бороться с указателями
(2) Мое личное убеждение состоит в том, что вся переменная должна содержать только действительные значения во все времена. После удаления/освобождения указатель уже не является допустимым значением, поэтому его необходимо удалить из этой переменной. Замена его на NULL (единственное значение указателя, которое всегда действует) является хорошим шагом.
(3) Код никогда не умирает. Он всегда используется повторно и часто способами, которые вы не представляли себе в то время, когда вы его написали. Ваш сегмент кода может быть скомпилирован в контексте С++ и, вероятно, будет перемещен в деструктор или метод, вызываемый деструктором. Взаимодействие виртуальных методов и объектов, находящихся в процессе разрушения, - это тонкие ловушки даже для очень опытных программистов.
(4) Если ваш код в конечном итоге используется в многопоточном контексте, другой поток может прочитать эту переменную и попытаться получить к ней доступ. Такие контексты часто возникают, когда устаревший код обернут и повторно используется на веб-сервере. Таким образом, еще лучший способ освобождения памяти (с параноидальной точки зрения) состоит в том, чтобы (1) скопировать указатель на локальную переменную, (2) установить исходную переменную в NULL, (3) удалить/освободить локальную переменную.
Ответ 5
Если указатель будет повторно использован, то после использования он должен быть установлен на 0 (NULL), даже если объект, который он указывает, не освобождается от кучи. Это позволяет корректно проверять значение NULL как if (p) {//делать что-то}. Также только потому, что вы освобождаете объект, адрес которого указывает указатель, не означает, что указатель установлен 0 после вызова ключевого слова delete или свободной функции вообще.
Если указатель используется один раз, и он является частью области действия, которая делает ее локальной, тогда нет необходимости устанавливать ее в NULL, поскольку она будет удаляться из стека после возвращения функции.
Если указатель является членом (структурой или классом), вы должны установить его в NULL после освобождения объекта или объектов на двойном указателе для правильной проверки против NULL.
Выполнение этого поможет вам облегчить головные боли от недействительных указателей, таких как "0xcdcd..." и т.д. Поэтому, если указатель равен 0, вы знаете, что он не указывает на адрес и может убедиться, что объект выпущен из кучи.
Ответ 6
Оба очень важны, так как они относятся к поведению undefined. Вы не должны оставлять какие-либо способы поведения undefined в своей программе. Оба могут привести к сбоям, развращению данных, тонким ошибкам, любым другим плохим последствиям.
Оба довольно сложно отлаживать. Оба нельзя избежать наверняка, особенно в случае сложных структур данных. В любом случае вам намного лучше, если вы будете следовать следующим правилам:
- всегда инициализировать указатели - установите их в NULL или какой-либо действительный адрес
- после вызова free() установите указатель в NULL
- проверить любые указатели, которые могут быть NULL для фактического наличия NULL перед разыменованием их.
Ответ 7
В С++ можно было поймать оба путем реализации собственного умного указателя (или получения из существующих реализаций) и реализации чего-то вроде:
void release() {
assert(m_pt!=NULL);
T* pt = m_pt;
m_pt = NULL;
free(pt);
}
T* operator->() {
assert(m_pt!=NULL);
return m_pt;
}
В качестве альтернативы, в C вы могли бы по крайней мере предоставить два макроса для одного и того же эффекта:
#define SAFE_FREE(pt) \
assert(pt!=NULL); \
free(pt); \
pt = NULL;
#define SAFE_PTR(pt) assert(pt!=NULL); pt
Ответ 8
Эти проблемы чаще всего являются симптомами для гораздо более глубоких проблем. Это может произойти для всех ресурсов, требующих приобретения и последующего выпуска, например. память, файлы, базы данных, сетевые подключения и т.д. Основная проблема заключается в том, что вы потеряли информацию о распределении ресурсов по отсутствующей структуре кода, бросая случайные маллоки и освобождая всю базу кода.
Организуйте код вокруг DRY - не повторяйте себя. Держите связанные вещи вместе. Делайте только одно, и делайте это хорошо. "Модуль", который выделяет ресурс, несет ответственность за его освобождение и должен обеспечивать функцию для этого, которая также заботится о указателях. Для любого конкретного ресурса у вас есть ровно одно место, где оно выделено, и одно место, где оно было выпущено, оба близки друг к другу.
Предположим, вы хотите разбить строку на подстроки. Прямо используя malloc(), ваша функция должна заботиться обо всем: анализируя строку, выделяя нужный объем памяти, копируя подстроки там и, и. Сделайте функцию достаточно сложной, и не вопрос, потеряете ли вы ресурсы, но когда.
Ваш первый модуль заботится о фактическом распределении памяти:
void *MemoryAlloc (size_t size)
void MemoryFree (void *ptr)
Это ваше единственное место во всей вашей кодовой базе, где вызывается malloc() и free().
Затем нам нужно выделить строки:
StringAlloc (char **str, size_t len)
StringFree (char **str)
Они заботятся о необходимости len + 1 и что при освобождении указателя установлено значение NULL. Предоставьте другую функцию для копирования подстроки:
StringCopyPart (char **dst, const char *src, size_t index, size_t len)
Позаботится, чтобы индекс и len находились внутри строки src и изменяли его, когда это необходимо. Он вызовет StringAlloc для dst, и ему будет нужно, чтобы dst был правильно завершен.
Теперь вы можете написать свою функцию split. Вам больше не нужно заботиться о деталях низкого уровня, просто проанализируйте строку и получите подстроки из нее. Большая часть логики теперь находится в модуле, где он принадлежит, вместо того, чтобы смешиваться в одном большом чудовище.
Конечно, это решение имеет свои проблемы. Он предоставляет уровни абстракции, и каждый слой, одновременно решая другие проблемы, имеет свой собственный набор.
Ответ 9
На самом деле нет "более важной" части, к какой из двух проблем вы пытаетесь избежать. На самом деле вам действительно нужно избегать обоих, если вы хотите написать надежное программное обеспечение. Также очень вероятно, что любой из вышеперечисленных действий приведет к повреждению данных, если ваш веб-сервер будет pwned и другие развлечения в этих строках.
Существует еще один важный шаг, который нужно помнить - установка указателя на NULL после освобождения - это всего лишь половина работы. В идеале, если вы используете эту идиому, вы также должны обернуть доступ указателя таким образом:
if (ptr)
memcpy(ptr->stuff, foo, 3);
Просто установка самого указателя на NULL приведет только к сбою программы в непереносимых местах, что, вероятно, лучше, чем молчащее повреждение данных, но оно все еще не то, что вы хотите.
Ответ 10
Там нет гарантии, что программа выйдет из строя при доступе к указателю NULL.
Возможно, не по стандарту, но вам будет трудно найти реализацию, которая не определяет ее как незаконную операцию, которая вызывает сбой или исключение (в зависимости от среды выполнения).