释放它们之后,真的应该设置指向NULL的指针吗?

在释放它们之后,似乎有两个参数为什么应该设置一个指向NULL的指针。

当释放双指针时避免崩溃。

简而言之:无意中调用free() ,当它被设置为NULL时不会崩溃。

  • 几乎总是这样掩盖了一个逻辑错误,因为没有理由再次调用free() 。 让应用程序崩溃并修复它会更安全。

  • 不能保证崩溃,因为有时候新的内存被分配在相同的地址。

  • 当有两个指向相同地址的指针时,主要发生双重空闲。

逻辑错误也可能导致数据损坏。

避免重复使用已释放的指针

简而言之:访问释放的指针可能会导致数据损坏,如果malloc()在相同的位置分配内存,除非释放的指针设置为NULL

  • 如果偏移量足够大( someStruct->lastMembertheArray[someBigNumber] ), theArray[someBigNumber]不能保证程序在访问NULL指针时崩溃。 而不是崩溃会有数据损坏。

  • 将指针设置为NULL不能解决具有相同指针值的不同指针的问题。

问题

这里有一个post,在释放后盲目地将一个指针设置为NULL

  • 哪一个更难debugging?
  • 有两种可能性吗?
  • 这种错误有多大可能导致数据损坏而不是崩溃?

随意扩大这个问题。

第二个方法更重要:重新使用已释放的指针可能是一个微妙的错误。 你的代码保持正常工作,然后崩溃没有明确的理由,因为一些看似不相关的代码写在内存中,重用指针正好指向。

我曾经不得不在别人写的一个非常错误的程序上工作。 我的直觉告诉我,很多错误都是在释放内存后马虎不离的尝试继续使用指针。 我修改了代码,释放内存后将指针设置为NULL,并且bam ,空指针exception开始了。 在我修复了所有的空指针exception之后,代码更加稳定了。

在我自己的代码中,我只调用我自己的函数,这是free()的一个包装。 它需要一个指针指针,并在释放内存后将指针置零。 在调用free之前,它调用Assert(p != NULL); 所以它仍然捕获尝试来释放相同的指针。

我的代码也做了其他的事情,比如(在仅用DEBUG编译时)在分配内存后立即填充一个明显的值,在调用free()之前做同样的事情,以防有指针的副本等等。 。

编辑:每个请求,这里是示例代码。

 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()的空指针时,没有什么不好的事情发生,因为它检查指针,如果它已经是空的,就返回。

我的实际函数名称比较简洁,但是我试图为这个例子select自我logging的名字。 另外,在我的实际代码中,我调用free()之前,只有debugging代码填充缓冲区的值为0xDC ,所以如果我有一个额外的指针,相同的内存(一个不会被清除),它变得真的显然它指向的数据是假数据。 我有一个macrosDEBUG_ONLY() ,它在非debugging版本上编译为无; 和一个在结构上做sizeof()的macrosFILL() 。 这两个工作同样好: sizeof(FOO)sizeof(*pfoo) 。 所以这里是FILL()macros:

 #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 

我不这样做。 我不特别记得任何如果我这样做会更容易处理的错误。 但是这取决于你如何编写你的代码。 大概有三种情况我可以免费获得任何东西:

  • 当持有它的指针即将超出范围,或者是即将超出范围或被释放的对象的一部分时。
  • 当我用新的replace对象(例如重新分配)。
  • 当我释放一个可选的对象时。

在第三种情况下,您将指针设置为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,但从长远来看是错误的。

将鼠标指针隐藏在双倍空白处的错误也是事实。 第二个自由没有立即伤害,如果你做空指针,但从长远来看是错误的(因为它背离了事实,你的对象生命周期被打破)。 当你释放它们时,你可以断言它们是非空的,但是这会导致下面的代码释放一个包含可选值的结构体:

 if (thing->cached != NULL) { assert(thing->cached != NULL); free(thing->cached); thing->cached = NULL; } free(thing); 

那个代码告诉你的是,你太过分了。 它应该是:

 free(thing->cached); free(thing); 

我说,如果指针保持可用,则为空指针。 如果它不再可用,最好不要使它看起来是错误的,通过放入一个潜在的有意义的值,如NULL。 如果你想引发页面错误,使用一个不依赖于平台的平台依赖的值,但是你的代码的其余部分不会被视为一个特殊的“一切都很好,很花哨”值:

 free(thing->cached); thing->cached = (void*)(0xFEFEFEFE); 

如果在系统中找不到任何这样的常量,则可以分配不可读和/或不可写的页面,并使用该页面的地址。

如果不将指针设置为NULL,那么就有一个不小的机会,即应用程序继续以未定义的状态运行,然后在完全不相关的点上崩溃。 然后,你会花费大量的时间来debugging一个不存在的错误,然后才能发现,这是从前面的内存腐败。

我将这个指针设置为NULL,因为如果你没有将它设置为NULL,那么你的命中率就会比正确的点高。 第二次释放内存的逻辑错误还有待考虑,并且您的应用程序不会在具有足够大的偏移量的空指针访问上崩溃的错误在我看来是完全学术的,但并非不可能。

结论:我会去设置指针为NULL。

答案取决于(1)项目规模,(2)代码的预期寿命,(3)团队规模。 在一个短生命周期的小项目中,你可以跳过将指针设置为NULL,然后一起debugging。

在一个长寿命的项目中,有很好的理由将指针设置为NULL:(1)防御性编程总是好的。 你的代码可能没问题,但是隔壁的初学者可能仍然很难用指针(2)我个人的看法是,所有的variables都应该只包含有效值。 在删除/释放之后,指针不再是有效的值,所以需要从该variables中删除。 用NULLreplace它(唯一的总是有效的指针值)是一个好的步骤。 (3)代码永不死亡。 它总是被重用,而且通常是在你编写它的时候没有想到的。 你的代码段可能最终被编译成一个C ++上下文,并可能被移动到析构函数或被析构函数调用的方法。 即使是经验丰富的程序员,虚拟方法和正在被破坏的对象之间的交互也是微妙的陷阱。 (4)如果你的代码在multithreading上下文中被使用,其他一些线程可能会读取该variables并尝试访问它。 传统代码在Web服务器中被打包和重用时,通常会出现这样的上下文。 所以,从偏执的angular度来说,释放内存的更好方法是(1)将指针复制到局部variables,(2)将原始variables设置为NULL,(3)删除/释放局部variables。

两者都是非常重要的,因为他们处理不确定的行为。 您不应该在程序中留下任何未定义的行为。 两者都可能导致崩溃,破坏数据,微妙的错误,任何其他不良后果。

两者都很难debugging。 两者都无法避免,特别是在数据结构复杂的情况下。 无论如何,如果你遵循以下规则,你会变得更好:

  • 总是初始化指针 – 将它们设置为NULL或一些有效的地址
  • 在你调用free()之后,将指针设置为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中,你至less可以提供两个macros来达到同样的效果:

 #define SAFE_FREE(pt) \ assert(pt!=NULL); \ free(pt); \ pt = NULL; #define SAFE_PTR(pt) assert(pt!=NULL); pt 

这些问题往往只是一个更深层次的问题的症状。 这可能会发生在所有需要采购和更高版本的资源上,例如内存,文件,数据库,networking连接等等。核心问题是你丢失了一个丢失的代码结构的资源分配跟踪,抛出随机mallocs和释放所有的代码库。

围绕DRY组织代码 – 不要重复自己。 把相关的东西放在一起 只做一件事,做好事。 分配一个资源的“模块”负责释放它,并且也提供这样做的function,以保持指针。 对于任何特定的资源,你只需要一个地方分配一个地方,一个地方放在一起。

假设你想把一个string分割成子string。 直接使用malloc(),你的函数必须关心的一切:分析string,分配适量的内存,在那里复制子string,然后和和。 使function足够复杂,而不是你会失去资源的轨道,而是什么时候的问题。

你的第一个模块负责实际的内存分配:

 void *MemoryAlloc (size_t size) void MemoryFree (void *ptr) 

malloc()和free()被调用的时候,你的整个代码库中只有你自己的地方。

然后我们需要分配string:

 StringAlloc (char **str, size_t len) StringFree (char **str) 

他们注意,len + 1是需要的,而且指针在释放时被设置为NULL。 提供另一个函数来复制一个子string:

 StringCopyPart (char **dst, const char *src, size_t index, size_t len) 

如果index和len在srcstring内部并且在需要的时候修改它,它会很小心。 它将为dst调用StringAlloc,它会关心dst被正确终止。

现在你可以编写你的分割function。 您不必再关心低级细节,只需分析string并从中获取子string即可。 大部分逻辑现在在它所属的模块中,而不是在一个巨大的怪物中混合在一起。

当然这个解决scheme有它自己的问题。 它提供了抽象层,而每一层在解决其他问题的同时,都有自己的一套。

如果指针将被重用,则应该在使用后将其设置回0(NULL),即使指向的对象没有从堆中释放。 这允许有效的检查NULL,如if(p){//做某事}。 也就是因为你释放了指针所指向的地址的对象,并不意味着在调用delete关键字或free函数之后指针被设置为0。

如果指针被使用了一次,并且它是使其成为本地的作用域的一部分,则不需要将其设置为NULL,因为在函数返回之后它将被从堆栈中丢弃。

如果指针是成员(结构体或类),则应该在释放一个双指针上的对象或对象之后再次将其设置为NULL,以便对NULL进行有效检查。

这样做可以帮助你减轻“0xcdcd …”等无效指针的麻烦。 所以如果指针是0,那么你知道它不是指向一个地址,并且可以确保该对象是从堆中释放的。

你试图避免的两个问题中的哪一个没有什么“更重要的”部分。 如果你想编写可靠的软件,你真的需要避免这两者。 上述任何一种情况都很可能导致数据损坏,让您的networking服务器瘫痪,并沿着这些线路获得其他乐趣。

还有一个重要的步骤要记住 – 释放后指针设置为NULL只有一半的工作。 理想情况下,如果你使用这个习惯用法,你也应该像下面这样包装指针访问:

 if (ptr) memcpy(ptr->stuff, foo, 3); 

只要将指针本身设置为NULL,只会让程序崩溃在不合时宜的地方,这可能比静静地破坏数据更好,但它仍然不是你想要的。

当访问NULL指针时,不能保证程序崩溃。

也许不是标准,但是你很难find一个实现,它没有将其定义为导致崩溃或exception的非法操作(适合运行时环境)。