c-释放指针后,是否应该将指针真正设置为“ NULL”?
似乎有两个争论,为什么一个人在释放它们后应该设置一个指向NULL
的指针。
避免在两次释放指针时崩溃。
简介:设置为someStruct->lastMember
时,意外地第二次致电NULL
不会崩溃。
几乎总是掩盖一个逻辑错误,因为没有理由第二次致电
NULL
。 让应用程序崩溃并能够对其进行修复是更安全的。它不能保证会崩溃,因为有时会在同一地址分配新的内存。
当有两个指针指向同一地址时,通常会发生双重释放。
逻辑错误也可能导致数据损坏。
避免重用释放的指针
简短说明:如果NULL
在同一位置分配内存,则访问释放的指针可能导致数据损坏,除非将释放的指针设置为someStruct->lastMember
如果偏移量足够大(
someStruct->lastMember
,theArray[someBigNumber]
),则无法保证程序在访问NULL
指针时崩溃。 不会崩溃,不会有数据损坏。将指针设置为
NULL
不能解决具有相同指针值的其他指针的问题。
问题
这是一个有关释放后盲目将指针设置为NULL
的帖子。
- 哪一个更难调试?
- 有可能两者兼得吗?
- 这些错误导致数据损坏而不是崩溃的可能性有多大?
随意扩展这个问题。
第二个方法更重要:重新使用释放的指针可能是一个微妙的错误。 您的代码可以继续正常工作,然后由于没有明确的原因而崩溃,因为一些看似无关的代码写在内存中,重用的指针恰好指向该代码。
我曾经不得不从事别人写的一个真正有漏洞的程序。 我的直觉告诉我,许多错误与释放内存后草率地尝试使用指针有关; 我修改了代码,以在释放内存后将指针设置为NULL,并且bam,空指针异常开始到来。 修复所有空指针异常之后,代码突然变得稳定得多。
在我自己的代码中,我只调用自己的函数,该函数是围绕free()的包装。 它需要一个指向指针的指针,并在释放内存后使该指针为空。 并且在调用free之前,它会调用FreeAnything()
,因此它仍会捕获对同一指针进行两次释放的尝试。
我的代码还执行其他操作,例如(仅在DEBUG构建中)在分配内存后立即使用明显的值填充内存,在指针存在副本的情况下,在调用FreeAnything()
之前执行相同的操作,等等。详细信息。
编辑:每个请求,这是示例代码。
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()
。这是因为FILL()
会抱怨一个空指针。 我有那个断言是为了发现尝试双重释放的尝试,但是我认为它实际上并没有为我带来很多错误。 如果要忽略断言,则可以省去检查并仅调用0xDC
。除了断言,当您尝试使用DEBUG_ONLY()
释放空指针时,不会发生任何不好的情况,因为它检查指针并返回是否返回 已经为空。
我的实际函数名称比较简洁,但是在此示例中,我尝试选择自记录名称。 另外,在我的实际代码中,我有仅调试的代码,该代码在调用0xDC
之前用值FILL()
填充缓冲区,以便如果我有一个额外的指针指向该相同的内存(一个不会被清空的内存),则很明显 它指向的数据是伪数据。 我有一个宏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
我不这样做 我没有特别记得的是,如果有的话,任何更容易处理的错误。 但这实际上取决于您如何编写代码。 我大约有三种情况可以释放任何东西:
- 当持有它的指针即将超出范围时,或者是将要超出范围或被释放的对象的一部分。
- 当我用一个新对象替换该对象时(例如,与重新分配一样)。
- 当我释放可能存在的对象时。
在第三种情况下,将指针设置为NULL。 这并不是专门因为要释放它,而是因为what-it-是可选的,因此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插入可使它更明显。 如果不使指针为NULL,则取消引用可能不会立即造成危害,但从长远来看是错误的。
的确,指针为NULL会掩盖您双重释放的错误。 如果您使指针为NULL,则第二个free不会立即造成损害,但从长远来看是错误的(因为它背叛了对象生命周期已损坏的事实)。 释放它们时,您可以断言事物为非空,但这会导致以下代码释放包含可选值的结构:
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);
如果在系统上找不到任何此类常量,则可以分配一个不可读和/或不可写的页面,并使用该地址。
如果您不将指针设置为NULL,那么您的应用程序将继续以未定义状态运行,并随后在完全不相关的位置崩溃,这是一个很小的机会。 然后,您将花费大量时间来调试一个不存在的错误,然后才能发现这是早期的内存损坏。
我将指针设置为NULL的原因是,与未将其设置为NULL的机会相比,您更有可能更早地找到错误的正确位置。 仍然需要考虑第二次释放内存的逻辑错误,在我看来,应用程序不会在具有足够大偏移量的空指针访问时不会崩溃的错误是完全学术性的,尽管并非不可能。
结论:我将把指针设置为NULL。
答案取决于(1)项目规模,(2)代码的预期寿命,(3)团队规模。在寿命短的小型项目中,可以跳过将指针设置为NULL的操作,而只是进行调试。
在一个长期的大型项目中,有充分的理由将指针设置为NULL:(1)防御性编程总是好的。您的代码可能没问题,但是隔壁的初学者可能仍然无法使用指针(2)我个人认为,所有变量始终应仅包含有效值。删除/释放后,指针不再是有效值,因此需要将其从该变量中删除。用NULL(唯一始终有效的指针值)代替它是一个好步骤。(3)代码永不消亡。它总是被重用,并且经常以您编写时没有想到的方式被重用。您的代码段可能最终会在C ++上下文中进行编译,并且可能移至析构函数或被析构函数调用的方法。即使对于经验丰富的程序员,虚拟方法和正在被破坏的对象之间的交互也是一个微妙的陷阱。(4)如果您的代码最终在多线程上下文中使用,则其他某个线程可能会读取该变量并尝试访问它。当将遗留代码包装在Web服务器中并在其中重用时,通常会出现这种情况。因此,从偏执狂的角度来看,释放内存的一种更好的方法是(1)将指针复制到局部变量,(2)将原始变量设置为NULL,(3)删除/释放局部变量。
如果要重用该指针,则即使未将指向的对象从堆中释放,也应在使用后将其设置回0(NULL)。 这允许对NULL进行有效检查,例如if(p){//做某事}。 同样,仅仅因为释放了指针所指向的地址的对象,并不意味着在完全调用delete关键字或free函数之后指针就被设置为0。
如果指针仅使用一次,并且是使它成为局部作用域的一部分,则无需将其设置为NULL,因为它将在函数返回后从堆栈中将其丢弃。
如果指针是成员(结构或类),则应在再次释放双指针上的一个或多个对象后,将其设置为NULL,以针对NULL进行有效检查。
这样做可以帮助您减轻“ 0xcdcd ...”等无效指针的困扰。 因此,如果指针为0,那么您就知道它没有指向地址,并且可以确保从堆中释放对象。
两者都很重要,因为它们处理不确定的行为。 您不应在程序中留下任何未定义行为的方法。 两者都可能导致崩溃,数据损坏,细微的错误以及任何其他不良后果。
两者都很难调试。 不能肯定避免两者,尤其是在复杂的数据结构的情况下。 无论如何,如果遵循以下规则,您的生活就会更好:
- 始终初始化指针-将它们设置为NULL或一些有效地址
- 调用free()后,将指针设置为NULL
- 在取消对它们的引用之前,请检查所有可能为NULL的指针实际上是否为NULL。
在C ++中,可以通过实现自己的智能指针(或从现有的实现派生)并实现类似的方式来捕捉这两种情况:
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
这些问题通常只是更深层问题的症状。 对于需要征用和以后发行的所有资源(例如 内存,文件,数据库,网络连接等。核心问题是,由于缺少代码结构,您对资源分配失去了跟踪,抛出了随机malloc,并释放了整个代码库。
围绕DRY组织代码-不要重复自己。 保持相关的东西在一起。 只做一件事情,并做好。 分配资源的“模块”负责释放它,还必须提供一个执行功能,以便也要照顾指针。 对于任何特定资源,您将恰好有一个分配资源的地方和一个释放资源的地方,两者都靠在一起。
假设您想将字符串拆分为子字符串。 直接使用malloc(),您的函数必须照顾所有事情:分析字符串,分配适当的内存量,在其中复制子字符串,以及and和。 使函数足够复杂,并且是否丢失资源不是问题,而是何时。
您的第一个模块负责实际的内存分配:
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)
如果index和len在src字符串中,请注意,并在需要时进行修改。 它会为dst调用StringAlloc,并且会关心dst是否正确终止。
现在,您可以编写您的split函数。 您不再需要关心底层细节,只需分析字符串并从中获取子字符串即可。 现在,大多数逻辑都在它所属的模块中,而不是在一个大型怪兽中混合在一起。
当然,这种解决方案有其自身的问题。 它提供了抽象层,每个层在解决其他问题的同时,也提供了自己的一组抽象层。
您要避免避免的两个问题中,确实没有“更重要”的部分。 如果您要编写可靠的软件,则确实需要避免两者。 以上两种情况中的任何一种也很可能导致数据损坏,网络服务器被伪装以及其他有趣的事情。
还有另外一个重要的步骤要牢记-在释放指针之后将指针设置为NULL只是工作的一半。 理想情况下,如果您使用此惯用语,则还应该将指针访问包装成如下形式:
if (ptr)
memcpy(ptr->stuff, foo, 3);
仅将指针本身设置为NULL只会使程序在不适当的地方崩溃,这可能比静默破坏数据更好,但仍然不是您想要的。
无法保证访问NULL指针时程序会崩溃。
也许不是按标准,但是很难找到一个实现,该实现未将其定义为导致崩溃或异常(适用于运行时环境)的非法操作。