在 C 中,为什么当使用 free() 释放结构时,结构的第一个成员经常"reset"为 0?



设置

假设我有一个struct father,它有一个成员变量,如int,另一个struct(所以father是一个嵌套struct)。这是一个示例代码:

struct mystruct {
int n;
};
struct father {
int test;
struct mystruct M;
struct mystruct N;
};

在 main 函数中,我们使用malloc()分配内存以创建一个struct father类型的新结构,然后我们填充它的成员变量和它的孩子:

struct father* F = (struct father*) malloc(sizeof(struct father));
F->test = 42;
F->M.n = 23;
F->N.n = 11;

然后,我们从structs 外部获得指向这些成员变量的指针:

int* p = &F->M.n;
int* q = &F->N.n;

之后,我们打印执行free(F)前后的值,然后退出:

printf("test: %d, M.n: %d, N.n: %dn", F->test, *p, *q);
free(F);
printf("test: %d, M.n: %d, N.n: %dn", F->test, *p, *q);
return 0;

这是一个示例输出(*):

test: 42, M.n: 23, N.n: 11
test: 0, M.n: 0, N.n: 1025191952

*: 使用 gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0

粘贴的完整代码:https://pastebin.com/khzyNPY1

问题

这是我用来测试如何使用free()释放内存的测试程序。我的想法(来自阅读K&R"8.7示例 - 存储分配器",其中实现并解释了free()的版本)是,当您free()struct时,您几乎只是告诉操作系统或程序的其余部分,您不会使用以前分配的内存中的特定空间malloc()。那么,在释放这些内存块之后,成员变量中应该有垃圾值,对吧?我可以看到测试程序中N.n发生这种情况,但是,随着我运行越来越多的样本,很明显,在绝大多数情况下,这些成员变量比任何其他"随机"值"重置"为 0 多。我的问题是:这是为什么?是因为堆栈/堆比任何其他值更频繁地填充零吗?


最后一点,这里有一些相关问题的链接,但不能回答我的特定问题:

  • C - 释放结构
  • 当你在malloc之后不自由时,到底会发生什么?

调用free后,指针Fpq不再指向有效内存。 尝试取消引用这些指针会调用未定义的行为。 事实上,这些指针的值在调用free后变得不确定,因此您也可以仅通过读取这些指针值来调用 UB。

由于取消引用这些指针是未定义的行为,因此编译器可以假定它永远不会发生,并基于该假设进行优化。

话虽如此,没有任何内容表明malloc/free实现必须保持存储在释放内存中的值不变或将它们设置为特定值。 它可能会将其内部簿记状态的一部分写入您刚刚释放的内存,也可能不会。 你必须查看glibc的来源才能确切地看到它在做什么。

除了未定义的行为和标准可能规定的任何其他内容之外,由于动态分配器是一个程序,因此固定了一个特定的实现,假设它不根据外部因素做出决策(它没有),该行为是完全确定的。

真正的答案:你在这里看到的是glibc分配器内部工作的影响(glibc是Ubuntu上默认的C库)。

分配的块的内部结构如下(来源):

struct malloc_chunk {
INTERNAL_SIZE_T      mchunk_prev_size;  /* Size of previous chunk (if free).  */
INTERNAL_SIZE_T      mchunk_size;       /* Size in bytes, including overhead. */
struct malloc_chunk* fd;                /* double links -- used only if free. */
struct malloc_chunk* bk;        
/* Only used for large blocks: pointer to next larger size.  */
struct malloc_chunk* fd_nextsize;       /* double links -- used only if free. */
struct malloc_chunk* bk_nextsize;
};

在内存中,当块正在使用(非自由)时,它看起来像这样:

chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|             Size of previous chunk, if unallocated (P clear)  |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|             Size of chunk, in bytes                     |A|M|P| flags
mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|             User data starts here...                          |

mchunk_prev_sizemchunk_size之外的每个字段仅在块空闲时才填充。这两个字段就在用户可用缓冲区之前。用户数据在mchunk_size之后开始(即在fd的偏移量处),并且可以任意大。如果前一个区块是空闲的,则mchunk_prev_size字段保存前一个区块的大小,而mchunk_size字段保存区块的实际大小(至少比请求的大小多 16 个字节)。

此处在库本身中提供了更全面的解释作为注释(如果您想了解更多信息,强烈建议阅读)。

当您free()一个块时,需要做出很多决定,即在哪里"存储"该块以进行簿记。通常,释放的块根据其大小分类到双链表中,以优化后续分配(可以从这些列表中获得已经可用且大小合适的块)。您可以将其视为一种缓存机制。

现在,根据您的glibc版本,它们的处理方式可能略有不同,并且内部实现非常复杂,但是在您的案例中发生的事情是这样的:

struct malloc_chunk *victim = addr; // address passed to free()
// Add chunk at the head of the free list
victim->fd = NULL;
victim->bk = head;
head->fd = victim;

由于您的结构基本上等效于:

struct x {
int a;
int b;
int c;
}

由于在你的机器sizeof(struct malloc_chunk *) == 2 * sizeof(int)上,第一个操作(victim->fd = NULL)有效地清除了结构的前两个字段的内容(记住,用户数据正好从fd开始),而第二个(victim->bk = head)正在改变第三个值。

该标准未指定在释放后使用指向已分配存储的指针的程序的行为。 实现可以通过指定比标准要求的更多的程序的行为来自由扩展语言,并且标准的作者旨在鼓励实现之间的多样性,这将在市场指导的实现质量基础上支持流行的扩展。 一些带有指向死对象的指针的操作得到了广泛的支持(例如,鉴于char *x,*y;如果程序在x为非 null 的情况下执行free(x); y=x;,则标准将允许符合的实现以任意方式运行,而不考虑在初始化后是否有任何东西对y执行任何操作,但大多数实现会扩展语言以保证如果从未使用y,此类代码将不起作用)但取消引用此类指针通常不是。

请注意,如果要将同一指针的两个副本传递给释放的对象:

int test(char *p1, char *p2)
{
char *q;
if (*p1)
{
q = malloc(0):
free(q);
return *p1+*p2;
}
else
return 0;
}

分配和释放 Q 的行为完全有可能干扰已分配给*p1(以及 *P2)的存储中的位模式,但不需要编译器来允许这种可能性。 编译器可能会合理地返回从 malloc/free 之前的*p1读取的值的总和,以及从它之后*p2读取的值的总和;即使p1p2相等,*p1+*p2也应该始终是偶数,此总和也可能是奇数。

调用free时会发生两件事:

  • 在 C 计算模型中,任何指向释放内存的指针值(无论是其开头,如F,还是其中的内容,如pq)都不再有效。C 标准不定义尝试使用这些指针值时会发生什么情况,并且编译器的优化可能会对程序的行为产生意外影响(如果尝试使用这些指针值)。
  • 释放的内存用于其他目的。使用它最常见的其他目的之一是跟踪可用于分配的内存。换句话说,实现mallocfree的软件需要数据结构来记录哪些内存块被释放和其他信息。当您free内存时,该软件通常会为此目的使用一些内存。这可能会导致您看到的变化。

释放的内存也可能被程序中的其他内容使用。在没有信号处理程序或类似东西的单线程程序中,通常没有软件会在free和准备参数之间运行printf,所以没有其他东西会如此快速地重用内存 -malloc软件重用是你观察到的最可能的解释。但是,在多线程程序中,内存可能会立即被另一个线程重用。(实际上,这可能有点不太可能,因为malloc软件可能会为单独的线程优先保留单独的内存池,以减少必要的线程间同步量。

释放动态分配的对象后,它不再存在。 任何后续访问它的尝试都具有未定义的行为。 因此,问题是无稽之谈:已分配struct的成员在宿主结构的生存期结束时不复存在,因此此时无法设置或重置它们。 没有有效的方法可以尝试确定此类不再存在的对象的任何值。

最新更新