调用free()包装器:解引用类型转换的指针将违反严格别名规则。

5

我尝试阅读了在SO上与标题类似的其他问题,但它们都稍微有点太复杂,以至于我无法将解决方案(甚至是解释)应用到自己的问题上,而我的问题似乎更简单。

在我的情况下,我有一个包装器围绕free(),在释放后将指针设置为NULL

void myfree(void **ptr)
{
    free(*ptr);
    *ptr = NULL;
}

在我正在工作的项目中,它被称为这样:
myfree((void **)&a);

这会使得gcc(在OpenBSD上的4.2.1版本)如果我将优化级别提高到-O3并添加-Wall(否则不会)时,发出警告"dereferencing type-punned pointer will break strict-aliasing rules"。
调用以下方式的myfree()不会使编译器发出该警告:
myfree((void *)&a);

因此,我在想我们是否应该改变调用myfree()的方式。

我认为第一种调用myfree()的方式存在未定义行为,但我还无法理解为什么会这样。此外,在我可以访问的所有编译器(clanggcc)和系统(OpenBSD、Mac OS X和Linux)上,这是唯一一个实际给出该警告的编译器和系统(我知道发出警告是一个很好的选择)。

使用两种调用方式之前、之中和之后打印指针的值,结果相同(但如果存在未定义行为,这可能意味着什么):

#include <stdio.h>
#include <stdlib.h>

void myfree(void **ptr)
{
    printf("(in myfree) ptr = %p\n", *ptr);
    free(*ptr);
    *ptr = NULL;
}

int main(void)
{
    int *a, *b;

    a = malloc(100 * sizeof *a);
    b = malloc(100 * sizeof *b);

    printf("(before myfree) a = %p\n", (void *)a);
    printf("(before myfree) b = %p\n", (void *)b);

    myfree((void **)&a);  /* line 21 */
    myfree((void *)&b);

    printf("(after myfree) a = %p\n", (void *)a);
    printf("(after myfree) b = %p\n", (void *)b);

    return EXIT_SUCCESS;
}

编译和运行它:

$ cc -O3 -Wall free-test.c
free-test.c: In function 'main':
free-test.c:21: warning: dereferencing type-punned pointer will break strict-aliasing rules

$ ./a.out
(before myfree) a = 0x15f8fcf1d600
(before myfree) b = 0x15f876b27200
(in myfree) ptr = 0x15f8fcf1d600
(in myfree) ptr = 0x15f876b27200
(after myfree) a = 0x0
(after myfree) b = 0x0

我希望了解第一个对myfree()的调用有何问题,并想知道第二个调用是否正确。谢谢。


我不建议在释放后总是将指针设置为NULL,因为这样你就看不到双重释放(这本不应该发生)。 - Arnaud Aliès
@mou 嗯,这是一个有道理的观点。反驳的论点是不将其设置为“NULL”可能会隐藏对已释放内存的访问。 - Kusalananda
是的,但是Valgrind和其他工具应该会告诉我们,因为我们正在谈论堆内存。 - Arnaud Aliès
@mou 调用 free(NULL) 是无害的,而解引用 NULL 则不是。因此我认为,(这是一个运行了多年的协作项目)被告知引用已释放内存的情况比捕获双重 free() 调用更好。我一直在考虑使用 Boehm GC 进行内存泄漏检测和/或 Valgrind,但由于我在项目中还比较新,所以还没有开始做这些事情。 - Kusalananda
3个回答

9
由于 a 是一个 int* 而不是一个 void*,所以 &a 不能被转换为指向 void* 的指针。(假设 void* 比指向整数的指针更宽,这是 C 标准允许的。)因此,你提出的两种替代方案 -- myfree((void**)a)myfree((void*)a) -- 都是不正确的。(将其强制转换为 void* 不是严格别名问题,但仍然会导致未定义行为。)
更好的解决方案(在我看来)是强制用户插入一个可见的赋值语句:
void* myfree(void* p) {
    free(p);
    return 0;
}

a = myfree(a);

使用clang 和 gcc,您可以使用属性来指示必须使用my_free的返回值,这样编译器会在您忘记分配时发出警告。或者您可以使用宏:

#define myfree(a) (a = myfree(a))

你的第一句话是不是意味着两个调用都不正确(因为ptr的类型是void **)? - Kusalananda
1
@kusalananda:是的,我应该将其添加到答案中。 - rici
我们尽可能地保持C99的标准,因此使用编译器特定的属性是不可取的。需要仔细考虑并与合作者讨论。 - Kusalananda
1
@kusalanada:处理属性的常规方法是在不支持它的实现中将 __attribute__ 定义为 #define(这不会改变程序行为;它只是一个警告)。我一直避免说这个,但个人而言,我不喜欢你正试图实现的习语,特别是当要被释放的指针本身在即将被释放的内存块中时。如果你想调试 double-free 或 use-after-free,请使用 valgrind。但很多人不同意我的看法。 - rici
我非常感谢这些评论。这是一份旧代码,仍在开发中(一个生物信息学应用程序),我正在清理其中的一些部分。这不是我试图实现某些东西,而是我试图使其正确。 - Kusalananda

4

这里有一个建议:

  1. 不违反严格别名规则。
  2. 使调用更自然。
void* myfree(void *ptr)
{
    free(ptr);
    return NULL;
}

#define MYFREE(ptr) ptr = myfree(ptr);

您可以简单地使用宏,如下所示:

int* a = malloc(sizeof(int)*10);

...

MYFREE(a);

2
基本上有几种方法可以让函数以与指针的目标类型无关的方式工作并修改指针:
  1. 将指针作为void*传递到函数中,并在调用站点上双向应用适当的转换,将其作为void*返回。这种方法的缺点是绑定了函数的返回值,使其无法用于其他目的,并且还排除了在锁内执行指针更新的可能性。
  2. 传递一个指向函数的指针,该函数接受两个void*,将其中一个强制转换为适当类型的指针,将另一个强制转换为该类型的双重间接指针,并可能使用第二个函数将传递的指针读取为void*,并使用这些函数来读取和写入相关的指针。这应该是100%可移植的,但可能非常低效。
  3. 在其他地方使用void*类型的指针变量和字段,并在实际使用它们时将它们强制转换为真正的指针类型,从而允许使用void**类型的指针来修改指针变量。
  4. 使用memcpy来读取或修改未知类型的指针,给出标识它们的双重间接指针。
  5. 文档说明代码是用C的一种方言编写的,它在1990年代很受欢迎,将“void **”视为任何类型的双重间接指针,并使用支持该方言的编译器和/或设置。C标准允许实现对不同类型的事物的指针使用不同的表示方法,因为那些实现不能支持通用的双重间接指针类型,而且因为那些实现可以轻松地允许使用void**,使它们在标准被撰写之前已经这样做了,所以没有必要让标准来描述这种行为。
在90%以上的实现中具有通用的双重间接指针类型的能力非常有用(并且仍然是),标准的作者当然知道这一点,但是作者对于描述明智的编译器编写者会支持的行为不太感兴趣,而是强制执行整体上有益的行为,即使在无法便宜支持它们的平台上也是如此(例如,即使在一个无符号数学指令包装模65535的平台上,编译器也必须生成需要的代码来使计算包装模65536)。我不确定现代编译器编写者为什么无法认识到这一点。
也许如果程序员开始公开编写明智的C方言,标准的维护者可能会认识到这些方言的价值。[请注意,从别名方面来看,将void**视为通用的双重间接指针将比强制程序员使用上述2-4中的任何一种替代方法具有更少的性能成本;因此,编译器编写者声称将void**视为通用的双重间接指针将杀死性能应该持怀疑态度]。

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