释放指针后真的应该将指针设置为“NULL”吗?

Posted

技术标签:

【中文标题】释放指针后真的应该将指针设置为“NULL”吗?【英文标题】:Should one really set pointers to `NULL` after freeing them? 【发布时间】:2010-12-25 04:08:33 【问题描述】:

似乎有两个论点为什么要在释放它们之后设置指向NULL 的指针。

Avoid crashing when double-freeing pointers.

简短:第二次调用free(),当它设置为NULL 时不会崩溃。

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

不保证会崩溃,因为有时会在同一地址分配新内存。

双释放主要发生在有两个指针指向同一个地址时。

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

Avoid reusing freed pointers

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

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

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

问题

这里是a post against blindly setting a pointer to NULL after freeing。

哪个更难调试? 是否有可能同时捕获两者? 此类错误导致数据损坏而不是崩溃的可能性有多大?

请随意扩展这个问题。

【问题讨论】:

欺骗***.com/questions/1879168/… 等等。 这篇文章中SO 1879168 的外部引用今天对我来说是坏的——SO 服务器抛出一个 404 页面。你能追踪并更新外部参考吗?或者提供一种或多种替代方案?似乎有很多可供选择。 相关(但不是骗子,因为 C != C++):***.com/questions/1931126/… @GeorgSchölly "如果malloc() 在同一位置分配内存,访问已释放的指针可能会导致数据损坏,除非已释放的指针设置为NULL" 数据将如何损坏我没有得到。 @AbhishekMane 评论部分可能不是进行此类讨论的最佳场所,也许会打开一个新问题?简而言之,不应再使用已释放的指针,因为内存可能会再次被其他内容填充。通过旧指针更改它意味着您更改了代码中其他地方使用的内存。 【参考方案1】:

答案取决于 (1) 项目规模,(2) 代码的预期生命周期,(3) 团队规模。 在一个生命周期很短的小项目上,你可以跳过将指针设置为 NULL,直接调试。

在一个大型的、长期存在的项目中,将指针设置为 NULL 有充分的理由: (1) 防御性编程总是好的。你的代码可能没问题,但隔壁的初学者可能仍然在为指针而苦恼 (2) 我个人的信念是,所有变量在任何时候都应该只包含有效值。删除/释放后,指针不再是有效值,因此需要从该变量中删除。将其替换为 NULL(唯一始终有效的指针值)是一个很好的步骤。 (3) 代码永不消亡。它总是被重用,而且通常以您在编写它时没有想到的方式重用。您的代码段最终可能会在 C++ 上下文中编译,并且可能会移动到析构函数或被析构函数调用的方法。即使对于非常有经验的程序员来说,正在被破坏的虚拟方法和对象的交互也是微妙的陷阱。 (4) 如果您的代码最终在多线程上下文中使用,其他线程可能会读取该变量并尝试访问它。当遗留代码在 Web 服务器中被包装和重用时,通常会出现这种情况。因此,释放内存的更好方法(从偏执的角度来看)是(1)将指针复制到局部变量,(2)将原始变量设置为 NULL,(3)删除/释放局部变量。

【讨论】:

我看不出你的多线程建议有什么帮助。如果另一个线程在您将其设置为 NULL 之前读取了指针值,然后您中断,将其设置为 NULL,释放它。然后调度程序中断你并重新调度原始线程,它仍然使用无效值。除非您使用某种锁定,否则您的释放线程执行操作的顺序并不重要,重要的是其他线程是否在您释放资源时首先使用它。【参考方案2】:

如果指针要被重用,那么即使它指向的对象没有从堆中释放,也应该在使用后将其设置回 0(NULL)。这允许对 NULL 进行有效检查,例如 if (p) //do something。同样,仅仅因为您释放了指针指向的地址的对象,并不意味着在调用 delete 关键字或 free 函数后指针被设置为 0。

如果指针被使用一次并且它是使其成为本地的范围的一部分,则无需将其设置为 NULL,因为它将在函数返回后从堆栈中释放。

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

这样做将帮助您减轻诸如“0xcdcd...”之类的无效指针带来的麻烦。因此,如果指针为 0,那么您就知道它没有指向地址,并且可以确保从堆中释放对象。

【讨论】:

【参考方案3】:

第二个更为重要:重用已释放的指针可能是一个微妙的错误。您的代码继续正常工作,然后无缘无故崩溃,因为一些看似无关的代码写入了重用指针恰好指向的内存中。

我曾经不得不在别人写的一个真的错误程序上工作。我的直觉告诉我,许多错误与释放内存后继续使用指针的草率尝试有关。我修改了代码以在释放内存后将指针设置为 NULL,并且 bam 开始出现空指针异常。在我修复了所有的空指针异常后,代码突然稳定了很多

在我自己的代码中,我只调用我自己的函数,它是 free() 的包装器。它需要一个指向指针的指针,并在释放内存后使指针为空。在调用 free 之前,它会调用 Assert(p != NULL);,因此它仍然会捕获双重释放同一指针的尝试。

我的代码也做其他事情,例如(仅在 DEBUG 构建中)在分配内存后立即用明显的值填充内存,在调用 free() 之前做同样的事情以防有指针的副本等.Details here.

编辑:根据请求,这里是示例代码。

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() 释放空指针时不会发生任何不好的事情,因为它会检查指针并在它已经为空时返回。

我的实际函数名称更简洁,但我尝试为这个示例选择自记录名称。此外,在我的实际代码中,我有一个仅调试代码,它在调用 free() 之前用值 0xDC 填充缓冲区,这样如果我有一个指向同一内存的额外指针(一个不会被清空的)它很明显,它指向的数据是虚假数据。我有一个宏,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

【讨论】:

当人们使用空指针执行 free 时,您的断言会导致问题,这对 C 来说是可以的(如果不是总是好的甚至是必要的)。非空指针上的双重释放是一个问题,但是在您的情况下,您会同时捕获该情况和一些有效情况,对吗? “并不总是好的甚至是必要的”?从来没有必要释放空指针。是的,断言将在不会发生实际伤害的情况下触发。我不得不承认,我认为 assert 从未在我的代码中发现错误。按照我的设置方式,如果我尝试两次释放指针,我会得到一个断言,但我似乎没有犯这个错误。如果您查看“此处的详细信息”链接,您可以了解我用来保持 C 代码无错误的技巧,其他技巧比 null free() 上的断言更有价值。最有价值的是经常被检查的“签名”。 但有时变量可能为空,也可能不为空,具体取决于之前的一些逻辑。而不是做 if (p) free(p);你可以只使用免费(p)。它是 C 标准的一部分,我会保留这个约定 IMO。 我的意思是,尝试两次释放同一个指针是一个实际错误。我想检测这个错误。但是在我的代码中,当你第一次释放指针时,它会被清空;所以第二次,它是一个空指针。因此断言。但我似乎没有尝试双重释放东西,而且我认为断言从未为我发现过错误,所以我必须承认它没有那么有价值。在 free 上清空指针,并在释放内存之前擦除内存,都是非常有价值的;当一个断言触发并说“嘿,笨蛋,你这里有一个错误!”时,我喜欢它。无需运行调试器。 我真的不应该发布我在公司领取薪水时编写的代码。它属于支付我工资的公司。但是我可以从头开始写同样的东西作为例子,我现在会在答案中这样做。【参考方案4】:

我不这样做。我不记得任何如果我这样做会更容易处理的错误。但这实际上取决于您如何编写代码。大约有三种情况我可以释放任何东西:

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

在第三种情况下,您将指针设置为 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 化可以使其更加明显。如果您不将指针设为 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);

如果您在系统上找不到任何此类常量,您可以分配一个不可读取和/或不可写入的页面,并使用该页面的地址。

【讨论】:

我的代码包含很多在#ifdef DEBUG 下编译的东西,所以我的DEBUG 构建非常小心,并且发布构建不会减慢。我的 DEBUG 构建用 0xDC 字节填充了 MALLOC 分配的所有内存; 0xFE 也可以。在释放一个结构之前,DEBUG 构建用 0xDC 填充该结构,并在释放之后将指针设置为 NULL。一两次我的完整性检查断言已经触发,因为我有一个指向我释放的内存的指针,并且覆盖空闲数据导致完整性检查失败。这比在调试器中花费数小时要好得多。 使用调试内存分配器,我明白了。您所描述的似乎与此非常接近。不过,就像您在对您的答案的评论中所说的那样,关于 free 上的 null 检查:“我认为断言从未发现过错误......如果我尝试释放指针两次,我将得到一个断言,但我似乎没有犯那个错误”。你的编码风格和实践远比免费的空指针更有价值。 @SteveJessop __"如果 malloc() 在同一位置分配内存,则访问已释放的指针可能会导致数据损坏,除非已释放的指针设置为 NULL __ 是真的吗?我认为 steveha 点对此有一些了解,但我无法理解。你能解释一下吗?【参考方案5】:

这些问题通常只是更深层次问题的症状。这可能发生在所有需要获取和稍后发布的资源上,例如内存、文件、数据库、网络连接等。核心问题是您因缺少代码结构而丢失了对资源分配的跟踪,在整个代码库中抛出随机 malloc 和释放。

围绕 DRY 组织代码 - 不要重复自己。把相关的东西放在一起。只做一件事,做好。分配资源的“模块”负责释放它,并且必须为此提供一个函数来保持对指针的关注。对于任何特定资源,您将拥有一个分配它的位置和一个释放它的位置,两者都靠得很近。

假设您要将字符串拆分为子字符串。直接使用 malloc(),你的函数必须关心所有事情:分析字符串,分配适当的内存量,复制子字符串,and and 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 是否正确终止。

现在您可以编写拆分函数了。您不必再关心底层细节,只需分析字符串并从中取出子字符串即可。大部分逻辑现在都在它所属的模块中,而不是混合在一起形成一个巨大的怪物。

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

【讨论】:

我知道我的评论来得太晚了,但这是唯一理智的答案。需要 NULL 出一个指针是一个更深层次问题的标志,即丢失分配和释放的跟踪。解释得很好。【参考方案6】:

在 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

【讨论】:

C 中存在运算符重载? 对不起,我没有看到与C相关的问题。 +1,防御性编程的概念,按照惯例可以在 C 中使用类似的断言。【参考方案7】:

无法保证程序在访问 NULL 指针时会崩溃。

也许不符合标准,但您很难找到一个不将其定义为导致崩溃或异常的非法操作(根据运行时环境而定)的实现。

【讨论】:

【参考方案8】:

如果您不将指针设置为 NULL,那么您的应用程序继续以未定义状态运行并随后在完全不相关的点崩溃的可能性并不小。然后,您将花费大量时间调试一个不存在的错误,然后才发现这是之前的内存损坏。

我将指针设置为 NULL,因为与未将其设置为 NULL 相比,您更早地找到错误的正确位置的可能性更高。第二次释放内存的逻辑错误仍然需要考虑,并且在我看来,您的应用程序不会在具有足够大偏移量的空指针访问时崩溃的错误是完全学术性的,尽管并非不可能。

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

【讨论】:

【参考方案9】:

对于您要避免的两个问题中的哪一个,实际上并没有“更重要”的部分。如果你想编写可靠的软件,你真的,真的需要避免两者。上述任何一种情况也很可能会导致数据损坏,让您的网络服务器被篡改以及其他类似的乐趣。

还有一个重要的步骤要记住——在释放指针后将指针设置为 NULL 只是完成了一半。理想情况下,如果你使用这个习惯用法,你还应该将指针访问包装成这样:

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

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

【讨论】:

【参考方案10】:

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

两者都很难调试。两者都无法避免,尤其是在复杂数据结构的情况下。无论如何,如果您遵循以下规则,您会过得更好:

始终初始化指针 - 将它们设置为 NULL 或某个有效地址 调用 free() 后,将指针设置为 NULL 在取消引用之前检查所有可能为 NULL 的指针是否实际为 NULL。

【讨论】:

为什么?,这篇帖子 ***.com/questions/1025589/… 声称设置指向 NULL 的指针通常没有帮助。 是的,在某些情况下它无济于事。但如果你总是留下悬空指针,情况会变得更糟。如您所知,安全带并不能保证一个人在车祸中幸存下来,但这并不意味着安全带完全没用。

以上是关于释放指针后真的应该将指针设置为“NULL”吗?的主要内容,如果未能解决你的问题,请参考以下文章

释放堆内存后将指针设置为 NULL [重复]

在 free() 之后将指针设置为 NULL 总是一个好习惯吗? [复制]

C 语言二级指针案例 ( 多级指针内存释放问题 | 多级指针避免野指针 )

为啥free函数不在释放内存后,将指针置NULL,野指针有啥用

free() 在 c 代码和代码中收集垃圾值即使在释放后将指针设置为 NULL 也不起作用

指针free后还存在吗?