悬空指针和双重释放问题

3

经过一些痛苦的经历,我理解了悬空指针和双重释放的问题。我正在寻求合适的解决方案。

aStruct 包括其他数组在内的多个字段。

aStruct *A = NULL, *B = NULL;
A = (aStruct*) calloc(1, sizeof(sStruct));
B = A;
free_aStruct(A);
...
// Bunch of other code in various places.
...
free_aStruct(B);

有没有办法编写free_aStruct(X),使得free_aStruct(B)能够优雅地退出?

void free_aStruct(aStruct *X) {
    if (X ! = NULL) {
        if (X->a != NULL) { free(X->a); x->a = NULL; }
        free(X); X = NULL;
    }
}

上述操作只有在调用free_aStruct(A); 时才会将A = NULL 。此时,B 成为“悬挂指针”。

如何避免或解决这种情况?引用计数是唯一可行的解决方案吗?还是有其他“防御性”方法可以释放内存,以防止free_aStruct(B); 导致程序崩溃?


1
移植到Java最好的一点是,当我这样做时,我曾经每天都要处理的所有类别问题完全消失了。 - Bill K
4
只有当你编写可能存在这些问题的代码时,才需要处理这些问题。 - anon
3
你的询问实际上是“如果我写了糟糕的代码,能否防止因为我的代码而导致不好的事情发生?” 我认为没有任何肯定的答案,只能说没有什么阳光、明亮、小马和兔子。 - Paul Nathan
@Paul Nathan:不要忘记独角兽和彩虹! - Fred Larson
@Neil Butterworth 只是回复你的观点,即只有在编写代码时才会出现这些问题 - 我几乎完全在处理一些相当严重的糟糕设计决策的代码上工作,而这些决策并不是我做出的。 - Bill K
显示剩余2条评论
5个回答

5
在普通的C语言中,解决这个问题最重要的方法是纪律性,因为问题的根源在于这里:
B = A;

在不改变结构体内部任何内容的情况下,复制指针,绕过编译器的任何警告。你需要使用类似这样的东西:

B = getref_aStruct(A);

下一个重要的事情是跟踪分配。一些有助于此的东西包括干净的模块化、信息隐藏和DRY——不要重复自己。你可以直接调用calloc()来分配内存,而使用free_aStruct()函数来释放它。最好使用create_aStruct()来分配它。这样可以将事物集中在一个地方,而不是在代码库中到处抛出内存分配。
这是构建在此基础上的任何内存跟踪系统的更好基础。

感谢您的回复。这似乎是一个明智的做法。我的aStruct实际上是一个递归数据结构,我有一个名为node-ownership(self-other)的标志。我有new_aStruct()函数,它将所有权标志设置为SELF。现在,free_aStruct()函数在释放子节点之前检查节点所有权标志是否已设置。如果需要,B= getref_aStruct(A)将是显式转移所有权的完美位置。这样,每次只有一个对象拥有子节点。getref_aStruct()强制执行纪律。再次感谢您的帮助。最好的祝福,Russ - user151410

2

我认为你不能自动完成这个任务,因为C语言要求你管理内存,并且你有责任确保引用和悬空指针得到处理!

void free_aStruct(aStruct *X){
  if (X != NULL){
      if (X->a != NULL){free(X->a); X->a = NULL;}
      free(X); X = NULL;
}
}

顺便说一下,在上面的 if 检查中有一个错别字...使用小写字母 'x' 而不是 'X' ...

当我看上面的代码时,我的想法是你正在对类型为 aStruct * 的指针变量的副本进行释放。我会将其修改为通过引用调用...

void free_aStruct(aStruct **X){
  if (*X != NULL){
      if (*X->a != NULL){
          free(*X->a); 
          *X->a = NULL;
      }
      free(*X); 
      *X = NULL;
  }
}

并像这样调用它:

free_aStruct(&A);

除此之外,无论是意外编码还是设计故障,你最终都要自己负责“悬空指针”...


谢谢,这真的很有帮助。那就是我所询问的内容。Russ - user151410

1

引用计数真的不难:

aStruct *astruct_getref(aStruct *m)
{
    m->refs++;
    return m;
}

aStruct *astruct_new(void)
{
    sStruct *new = calloc(1, sizeof *new);
    return astruct_getref(new);
}

void astruct_free(aStruct *m)
{
    if (--m->refs == 0)
        free(m);
}

(在多线程环境中,您还可能需要添加锁定。)

然后您的代码将会是:

aStruct *A = NULL, *B = NULL;
A = astruct_new();
B = astruct_getref(A);
astruct_free(A);
...
//bunch of other code in various places.
...
astruct_free(B);

您问到了锁定的问题。不幸的是,当涉及到锁定时,没有一种通用的解决方案 - 这完全取决于您的应用程序中有哪些访问模式。精心设计和深思熟虑是无可替代的。(例如,如果您可以保证没有线程在另一个线程的aStruct上调用astruct_getref()astruct_free(),那么引用计数根本不需要受到保护 - 上面的简单实现就足够了)。

话虽如此,上述原语可以轻松扩展以支持对astruct_getref()astruct_free()函数的并发访问:

aStruct *astruct_getref(aStruct *m)
{
    mutex_lock(m->reflock);
    m->refs++;
    mutex_unlock(m->reflock);
    return m;
}

aStruct *astruct_new(void)
{
    sStruct *new = calloc(1, sizeof *new);
    mutex_init(new->reflock);
    return astruct_getref(new);
}

void astruct_free(aStruct *m)
{
    int refs;

    mutex_lock(m->reflock);
    refs = --m->refs;
    mutex_unlock(m->reflock);
    if (refs == 0)
        free(m);
}

...但请注意,任何包含指向受并发访问的结构体的指针的变量也需要自己的锁定(例如,如果您有一个全局的aStruct *foo被同时访问,它将需要相应的foo_lock)。


谢谢caf。这在我的代码中肯定是可行的。你能提供一个锁定的例子吗?最好,Russ - user151410
@user151410:我更新了我的答案,添加了一些关于锁定的信息。 - caf
谢谢,那真的很有帮助。我准备尝试引用计数。您能推荐一些描述引用计数的 C 书籍吗?谢谢,Russ。 - user151410

1
即使您可以防止free_aStruct(B)崩溃,如果您的评论背后的代码中有任何对B的引用,那么将使用已被释放的内存,因此可能会在任何时候被新数据覆盖。仅“修复”free调用将只掩盖潜在错误。

好的观点。这是一个大问题。它主要是数字代码,并且有标志表明对象是干净的还是脏的,以防止意外访问。但是,正如本帖子中的帖子所指出的那样,在C语言中很难实施。我的希望是使用这些技术和各种工具(如valgrind等)来确保没有非法访问。Russ - user151410

1

有一些技巧可以使用,但底线是在 C 中无法严格执行任何操作。因此,我建议在开发过程中加入 valgrind(或 purify)。此外,一些静态代码分析器可能能够检测出其中的一些问题。


我正在使用Valgrind,它真的很有帮助。你能推荐一些开源的静态代码分析器吗?谢谢,Russ。 - user151410

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接