51

NULL似乎有两个论点为什么在释放它们之后应该设置一个指针。

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

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

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

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

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

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

避免重用释放的指针

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

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

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

问题

这是一篇反对NULL在释放后盲目设置指针的帖子。

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

随意扩展这个问题。

4

10 回答 10

27

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

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

在我自己的代码中,我只调用我自己的函数,它是 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()因为它会检查指针并在它已经为空时返回。

我的实际函数名称更简洁,但我尝试为这个示例选择自记录名称。0xDC此外,在我的实际代码中,我有只调试代码,它在调用之前用值填充缓冲区,free()这样如果我有一个指向同一内存的额外指针(一个不会被清空的),那么数据变得非常明显它指向的是虚假数据。我有一个宏,DEBUG_ONLY(),它在非调试版本中编译为空;FILL()和一个对结构执行 a的宏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
于 2009-12-10T08:53:40.930 回答
9

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

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

在第三种情况下,您将指针设置为 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);

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

于 2009-12-10T13:15:10.823 回答
4

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

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

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

于 2009-12-10T08:53:12.880 回答
3

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

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

于 2009-12-10T09:05:45.317 回答
2

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

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

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

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

于 2012-08-24T09:08:55.913 回答
1

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

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

  • 始终初始化指针 - 将它们设置为 NULL 或某个有效地址
  • 调用 free() 后将指针设置为 NULL
  • 在取消引用它们之前检查任何可能为 NULL 的指针实际上是否为 NULL。
于 2009-12-10T08:46:51.303 回答
1

在 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
于 2009-12-10T08:49:40.910 回答
1

这些问题通常只是更深层次问题的症状。这可能发生在所有需要获取和稍后发布的资源上,例如内存、文件、数据库、网络连接等。核心问题是您通过丢失的代码结构丢失了对资源分配的跟踪,抛出随机 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 是否正确终止。

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

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

于 2009-12-10T10:39:19.733 回答
0

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

还有一个重要的步骤要记住 - 在释放它之后将指针设置为 NULL 只是工作的一半。理想情况下,如果您正在使用这个习惯用法,您还应该将指针访问包装成如下内容:

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

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

于 2009-12-10T08:50:35.230 回答
0

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

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

于 2009-12-10T08:53:48.347 回答