这是释放内存的好方法吗?

Is this a good way to free memory?

释放struct Foo实例的函数如下:

void DestroyFoo(Foo* foo)
{
    if (foo) free(foo);
}

我的一位同事建议如下:

void DestroyFoo(Foo** foo)
{
    if (!(*foo)) return;
    Foo *tmpFoo = *foo;
    *foo = NULL; // prevents future concurrency problems
    memset(tmpFoo, 0, sizeof(Foo));  // problems show up immediately if referred to free memory
    free(tmpFoo);
}

我看到释放后将指针设置为 NULL 更好,但我不确定以下内容:

  1. 我们真的需要将指针分配给一个临时指针吗?它对并发性和共享内存有帮助吗?

  2. 将整个块设置为 0 以强制程序崩溃或至少输出具有显着差异的结果真的是个好主意吗?

如果函数被调用两次,您同事的建议将使代码成为 "safer"(请参阅 sleske 评论...因为 "safer" 对每个人来说可能并不相同...;- ).

使用您的代码,这很可能会崩溃:

Foo* foo = malloc( sizeof(Foo) );
DestroyFoo(foo);
DestroyFoo(foo); // will call free on memory already freed

使用您同事的代码版本,这不会崩溃:

Foo* foo = malloc( sizeof(Foo) );
DestroyFoo(&foo);
DestroyFoo(&foo); // will have no effect

现在,对于这种特定情况,执行 tmpFoo = 0;(在 DestroyFoo 内)就足够了。 memset(tmpFoo, 0, sizeof(Foo)); 如果 Foo 具有在释放内存后可能被错误访问的额外属性,将防止崩溃。

所以我会说是的,这样做可能是一种很好的做法....但这只是一种 针对有不良做法的开发人员的安全措施(因为绝对没有理由调用DestroyFoo 两次而不重新分配它)...最后,你使 DestroyFoo "safer" 但速度较慢(它做了更多的事情以防止它的错误使用)。

第二个解决方案似乎设计过度了。当然在某些情况下它可能更安全但是开销和复杂性太大了。

安全起见,释放内存后将指针设置为NULL。这始终是一个好习惯。

Foo* foo = malloc( sizeof(Foo) );
DestroyFoo(foo);
foo = NULL;

而且,我不知道为什么人们在调用 free() 之前检查指针是否为 NULL。这不是必需的,因为 free() 会为您完成这项工作。

将内存设置为 0(或其他)仅在某些情况下是一个好习惯,因为 free() 不会清除内存。它只会将内存区域标记为空闲,以便可以重复使用。如果你想清除内存,以便没有人能够读取它,你需要手动清理它。但这是相当繁重的操作,这就是为什么不应该使用它来释放所有内存的原因。在大多数情况下,释放而不清除就足够了,您不必牺牲性能来进行不必要的操作。

Do we really need to assign the pointer to a temporary one? Does it help in terms of concurrency and shared memory?

它与并发或共享内存无关。没意义。

Is it really a good idea to set the whole block to 0 to force the program to crash or at least to output results with significant discrepancy?

没有。完全没有。

您同事建议的解决方案很糟糕。原因如下:

  • 将整个块设置为 0 也没有任何效果。因为有人不小心使用了一个 free() 块,他们不会根据块中的值知道这一点。就是那种块calloc()returns。 所以不可能知道它是新分配的内存(calloc()malloc()+memset())还是之前被您的代码释放的内存。如果有的话,你的程序需要额外的工作来将每个空闲的内存块清零。

  • free(NULL); 是 well-defined 并且是 no-op,所以 if(ptr) {free(ptr);} 中的 if 条件没有实现。

  • 由于 free(NULL); 是 no-op,将指针设置为 NULL 实际上会隐藏该错误,因为如果某些函数实际上正在调用 free()在一个已经 free() 的指针上,那么他们就不会知道了。

  • 大多数用户函数在开始时都会进行 NULL 检查,并且可能不会考虑将 NULL 传递给它作为错误条件:

void do_some_work(void *ptr) {
    if (!ptr) {
        return; 
    }

   /*Do something with ptr here */
}

所以所有那些额外的检查和归零都给人一种假 "robustness" 的感觉,而它并没有真正改善任何东西。它只是用另一个问题代替了一个问题,即性能和代码膨胀的额外成本。

所以只调用 free(ptr); 而没有任何包装函数既简单又健壮(大多数 malloc() 实现会在双重释放时立即崩溃,这是 东西)。

"accidentally" 调用 free() 两次或更多没有简单的方法。程序员有责任跟踪所有分配的内存并适当地free()。如果有人觉得这很难处理,那么 C 语言可能不适合他们。

void destroyFoo(Foo** foo)
{
    if (!(*foo)) return;
    Foo *tmpFoo = *foo;
    *foo = NULL;
    memset(tmpFoo, 0, sizeof(Foo));
    free(tmpFoo);
}

你的同事代码不好,因为

  • 如果 fooNULL
  • 会崩溃
  • 创建额外的变量没有意义
  • 将值设置为零没有意义
  • 如果结构包含必须释放的内容,则直接释放结构不起作用

我想你的同事可能想到的是这个use-case

Foo* a = NULL;
Foo* b = createFoo();

destroyFoo(NULL);
destroyFoo(&a);
destroyFoo(&b);

既然如此,就应该是这样的。 try here

void destroyFoo(Foo** foo)
{
    if (!foo || !(*foo)) return;
    free(*foo);
    *foo = NULL;
}

首先我们需要看一下Foo,我们假设它看起来像这样

struct Foo
{
    // variables
    int number;
    char character;

    // array of float
    int arrSize;
    float* arr;

    // pointer to another instance
    Foo* myTwin;
};

现在要定义它应该如何销毁,让我们先定义它应该如何创建

Foo* createFoo (int arrSize, Foo* twin)
{
    Foo* t = (Foo*) malloc(sizeof(Foo));

    // initialize with default values
    t->number = 1;
    t->character = '1';

    // initialize the array
    t->arrSize = (arrSize>0?arrSize:10);
    t->arr = (float*) malloc(sizeof(float) * t->arrSize);

    // a Foo is a twin with only one other Foo
    t->myTwin = twin;
    if(twin) twin->myTwin = t;

    return t;
}

现在我们可以写一个与创建函数相对的销毁函数

Foo* destroyFoo (Foo* foo)
{
    if (foo)
    {
        // we allocated the array, so we have to free it
        free(t->arr);

        // to avoid broken pointer, we need to nullify the twin pointer
        if(t->myTwin) t->myTwin->myTwin = NULL;
    }

    free(foo);

    return NULL;
}

测试try here

int main ()
{
    Foo* a = createFoo (2, NULL);
    Foo* b = createFoo (4, a);

    a = destroyFoo(a);
    b = destroyFoo(b);

    printf("success");
    return 0;
}

不幸的是,这个想法行不通。

如果目的是捕获双倍释放,则不包括以下情况。

假设此代码:

Foo *ptr_1 = (FOO*) malloc(sizeof(Foo));
Foo *ptr_2 = ptr_1;
free (ptr_1);
free (ptr_2); /* This is a bug */

建议改为:

Foo *ptr_1 = (FOO*) malloc(sizeof(Foo));
Foo *ptr_2 = ptr_1;
DestroyFoo (&ptr_1);
DestroyFoo (&ptr_2); /* This is still a bug */

问题是第二次调用 DestroyFoo() 仍然会崩溃,因为 ptr_2 没有重置为 NULL,并且仍然指向已释放的内存。