代码之家  ›  专栏  ›  技术社区  ›  Georg Schölly

释放指针后是否真的应该将其设置为“null”?

  •  48
  • Georg Schölly  · 技术社区  · 6 年前

    似乎有两个理由可以说明为什么要将指针设置为 NULL 在释放他们之后。

    Avoid crashing when double-freeing pointers.

    短:呼叫 free() 第二次,意外地,当它被设置为 无效的 .

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

    • 它不一定会崩溃,因为有时新的内存被分配到同一个地址。

    • double free主要发生在有两个指针指向同一地址时。

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

    Avoid reusing freed pointers

    简而言之:如果访问释放的指针 malloc() 在同一位置分配内存,除非释放的指针设置为 无效的

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

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

    问题

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

    • 哪一个更难调试?
    • 两者都有可能抓住吗?
    • 这种错误导致数据损坏而不是崩溃的可能性有多大?

    请随意扩展这个问题。

    10 回复  |  直到 12 年前
        1
  •  25
  •   steveha    8 年前

    第二个更重要:重新使用释放的指针可能是一个微妙的错误。你的代码一直在工作,然后没有明确的原因崩溃,因为一些看起来不相关的代码写在内存中,重复使用的指针正好指向。

    我曾经不得不做一个 真正地 其他人写的小车程序。我的直觉告诉我,许多错误都与释放内存后不小心继续使用指针有关;我修改了代码,在释放内存后将指针设置为空,并且 巴姆 ,空指针异常开始出现。在我修复了所有的空指针异常之后,代码突然 许多的 更加稳定。

    在我自己的代码中,我只调用自己的函数,它是free()的包装器。它接受一个指向指针的指针,并在释放内存后使指针为空。在它叫自由之前,它叫 Assert(p != NULL); 所以它仍然捕捉到试图双重释放同一指针的情况。

    我的代码也做其他事情,例如(仅在调试构建中)在分配内存后立即用一个明显的值填充内存,在调用 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() 这会抱怨一个空指针。我有这个断言是为了检测一个双重释放的尝试,但我不认为它真的为我捕获了很多bug;如果你想漏掉断言,那么你可以漏掉支票,然后总是调用 免费() . 除了assert之外,当您尝试使用 免费() 因为它检查指针,如果指针已经为空则返回。

    我的实际函数名比较简洁,但是我尝试为这个示例选择自文档化的名称。另外,在我的实际代码中,只有调试代码才能用值填充缓冲区 0xDC 打电话之前 自由() 因此,如果我有一个指向同一内存的额外指针(一个不会被空出来的指针),那么很明显它指向的数据是假的数据。我有一个宏, DEBUG_ONLY() ,在非调试生成中编译为空;以及宏 FILL() 这样做了 sizeof() 在一个结构上。这两种方法同样有效: sizeof(FOO) sizeof(*pfoo) . 所以这里是 填充() 宏:

    #define FILL(p, b) \
        (memset((p), b, sizeof(*(p)))
    

    下面是一个使用 填充() 小精灵 调用前的值:

    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
    
        2
  •  7
  •   YeahStu    16 年前

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

    • 当持有它的指针即将超出作用域,或是即将超出作用域或被释放的对象的一部分时。
    • 当我用一个新的对象替换这个对象时(比如说重新分配)。
    • 当我释放一个可选的对象时。

    在第三种情况下,将指针设置为空。这并不是因为你要释放它,而是因为它是可选的,所以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;
    }
    

    如果有一个bug,在释放指针后试图取消对它的引用,那么将指针置空会使其更加明显。如果不使指针为空,则取消引用可能不会立即造成损害,但从长远来看是错误的。

    指针为空也是正确的 朦胧的 你的双倍自由。如果将指针设为空,则第二个free不会立即造成损害,但从长远来看是错误的(因为它暴露了对象生命周期被破坏的事实)。释放时,可以断言事物为非空,但这将导致以下代码释放包含可选值的结构:

    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);
    

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

        3
  •  3
  •   Kosi2801    16 年前

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

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

    结论:我倾向于将指针设置为空。

        4
  •  3
  •   Christoph Schiessl Joeyjoejoejr    12 年前

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

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

        5
  •  2
  •   bvrwoo_3376    13 年前

    如果指针将被重用,则在使用后应将is设置回0(空),即使它所指向的对象没有从堆中释放出来。这允许对空值进行有效的检查,比如if(p){//do something}。另外,仅仅因为释放了指针指向的地址的对象,并不意味着指针在调用delete关键字或free函数后设置为0。

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

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

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

        6
  •  1
  •   sharptooth    16 年前

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

    两者都很难调试。两者都是不可避免的,尤其是在复杂的数据结构中。不管怎样,如果你遵循以下规则,你会过得更好:

    • 始终初始化指针-将它们设置为空或一些有效地址
    • 调用free()后,将指针设置为空
    • 在取消引用任何可能为空的指针之前,请检查它们是否实际为空。
        7
  •  1
  •   Sebastian    16 年前

    在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
    
        8
  •  1
  •   Secure    16 年前

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

    把代码整理好,不要重复。把相关的东西放在一起。只做一件事,做好它。分配资源的“模块”负责释放资源,并且必须提供一个函数来执行此操作,这样也可以保持对指针的关注。对于任何特定的资源,您只有一个分配它的位置和一个释放它的位置,两者都很接近。

    假设要将字符串拆分为子字符串。直接使用malloc(),您的函数必须处理所有事情:分析字符串、分配适当的内存量、复制其中的子字符串,以及和。使函数变得足够复杂,而不是您是否会失去对资源的跟踪,而是何时。

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

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

    在整个代码库中,只有malloc()和free()被调用。

    然后我们需要分配字符串:

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

    它们注意需要len+1,并且释放时指针设置为空。提供另一个复制子字符串的函数:

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

    如果index和len在src字符串中,则需要小心,并在需要时对其进行修改。它将为dst调用stringalloc,并关心dst是否正确终止。

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

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

        9
  •  0
  •   Timo Geusch    16 年前

    在你试图避免的两个问题中,没有一个真正“更重要”的部分。如果你想编写可靠的软件,你真的,真的需要避免这两种情况。这也很有可能是上述任何一种情况都会导致数据损坏,使您的web服务器瘫痪,以及其他有趣的事情。

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

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

    仅仅将指针本身设置为null只会在不合适的地方导致程序崩溃,这可能比无声地破坏数据要好,但这仍然不是您想要的。

        10
  •  0
  •   Anon.    16 年前

    不能保证程序在访问空指针时崩溃。

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