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

时间:2009-12-10 08:43:26

标签: c pointers free

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

Avoid crashing when double-freeing pointers.

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

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

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

  • 当有两个指针指向同一地址时,大多数都会自由发生。

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

Avoid reusing freed pointers

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

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

  • 将指针设置为{{1}}无法解决具有相同指针值的不同指针的问题。

问题

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

  • 哪一个更难调试?
  • 是否有可能同时捕捉到它们?
  • 这样的错误导致数据损坏而不是崩溃的可能性有多大?

随意扩展此问题。

10 个答案:

答案 0 :(得分:25)

第二个更重要:重用已释放的指针可能是一个微妙的错误。您的代码保持正常工作,然后崩溃没有明确的原因,因为一些看似无关的代码在内存中写入了重用指针恰好指向的位置。

我曾经不得不处理别人写的真正的错误程序。我的直觉告诉我,许多错误都与在释放内存后继续使用指针的草率尝试有关;我修改了代码以在释放内存后将指针设置为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()释放一个空指针时,没有什么不好的事情发生,因为它检查指针并且只有在它已经为空时才返回。

我的实际函数名称相当简洁,但我试图为此示例选择自我记录的名称。另外,在我的实际代码中,我只调试代码,在调用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

答案 1 :(得分:7)

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

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

在第三种情况下,将指针设置为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这样的潜在有意义的值来使它看起来错误。如果你想引发一个页面错误,请使用一个与平台无关的值,这个值不是dereferancable,但是其余的代码不会将其视为一个特殊的“一切都很好,花花公子”的值:

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

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

答案 2 :(得分:3)

如果你没有将指针设置为NULL,那么你的应用程序将继续以未定义的状态运行,并在一个完全不相关的点上崩溃,这是一个非常小的可能性。然后你会花很多时间调试一个不存在的错误,然后才发现它是早先的内存损坏。

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

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

答案 3 :(得分:3)

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

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

答案 4 :(得分:2)

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

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

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

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

答案 5 :(得分:1)

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

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

  • 始终初始化指针 - 将它们设置为NULL或某个有效地址
  • 调用free()后将指针设置为NULL
  • 在解除引用之前检查任何可能为NULL的指针实际为NULL。

答案 6 :(得分: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

答案 7 :(得分:1)

这些问题通常只是更深层问题的症状。对于需要获取和后续发布的所有资源,例如,内存,文件,数据库,网络连接等。核心问题是你丢失了代码结构,丢失了资源分配的轨迹,抛出了随机的malloc并释放了整个代码库。

围绕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)

如果index和len在src字符串中并在需要时修改它,则需要注意。它将调用StringAlloc for dst,它会关心dst是否正确终止。

现在您可以编写分割功能了。您不必再关心低级别的细节,只需分析字符串并从中获取子字符串。大多数逻辑现在都在它所属的模块中,而不是在一个大的怪物中混合在一起。

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

答案 8 :(得分:0)

你要避免的两个问题中没有一个“更重要”的部分。如果你想编写可靠的软件,你真的,真的需要避免这两者。上述任何一种情况也很可能会导致数据损坏,让您的网络服务器变得笨拙以及其他方面的乐趣。

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

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

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

答案 9 :(得分:0)

  

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

可能不符合标准,但是你很难找到一个没有将它定义为导致崩溃或异常的非法操作的实现(适用于运行时环境)。