C语言中的free()函数是如何工作的?

12

可能是重复问题:
malloc()和free()的工作原理

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

int * alloc()
{
    int *p = (int *)calloc(5,4);
    printf("%d\n",p);
    return p;
}

int main()
{
 int *p = alloc();

 free(p);
 printf("%d\n",p);
 p[0] = 1;
 p[1] = 2;
 printf("%d %d\n",p[0],p[1]);
}

就这段代码而言,我首先分配了5个整型变量的内存空间。然后我释放了这部分内存。 当我使用printf输出p的值时,为什么p的值仍然等于这段内存空间的起始地址? 同时我也可以给p[0]和p[1]赋值。这是否意味着free()没有做任何事情?一旦我分配了内存,即使我已经释放它,我以后还可以使用它。


很多重复内容,例如如何使用malloc()和free()函数 - Paul R
7个回答

15

free释放指定地址处的内存。它不会改变变量p本身。然而,在此之后对p进行任何操作都是未定义的行为。如果您立即在释放后使用它,可能似乎有效,但仍然是完全错误的,并且可能导致崩溃或更糟。

free是与实现相关的。但是,在大多数实现中,它将写入堆中的簿记数据以记录该内存现在可用。例如,它可能标记某个块未使用,或将该块与相邻的空闲块合并。

请注意,对于指针使用%d也是未定义的。


13

Free并非什么也不做。它将内存返回给分配器,以便可以再次使用该内存。

你正在执行未定义的行为。你的程序可能看起来能工作,但后续可能会崩溃。


9

内存保护具有页面粒度,需要与内核交互

内存只能以页面为单位从程序中移除,即使这样也很少被观察到。

calloc(3)和malloc(3)会与内核交互以获取内存(如果需要)。但是大多数free(3)的实现不会将内存返回给内核1,它们只是将其添加到空闲列表中,以便calloc()和malloc()稍后可以重用释放的块。

即使free()想要将内存返回给系统,它也需要至少一个连续的内存页面,以便让内核实际保护该区域,因此释放小块只会在它是页面中的最后一个小块时导致保护更改。

因此,您的块仍然存在于空闲列表中。您可能能够像仍然分配一样访问它。C编译成机器代码,没有特殊的调试安排,对加载和存储没有合理性检查。现在,如果您尝试访问一个空闲块,则标准的行为是未定义的,以避免对库实现者提出不合理的要求。这里有各种各样的问题:

  • 有时分配器会维护单独的内存块,有时会使用它们在您的块之前或之后分配的标头(我猜是“页脚”),但它们可能只想使用块内存来保持空闲列表链接在一起。如果是这样,读取块是可以的,但其内容可能会更改,写入块可能会导致分配器出现故障或崩溃。
  • 当然,您的块将来可能会被分配,那么它很可能会被您的代码或库例程覆盖,或者被calloc()清零。
  • 如果重新分配块,则其大小也可能会更改,在这种情况下,还会在各个地方编写更多的链接或初始化。

1.很少有free()实现尝试将内存返回给系统并不一定是因为实现者偷懒。与内核交互比执行库代码要慢得多,而且好处很小。大多数程序具有稳态或增加的内存占用量,因此花费时间分析堆以寻找可返回的内存将完全浪费。其他原因包括内部碎片使得页面对齐块不太可能存在,而且返回块可能会将两侧的块分段。最后,很少有程序返回大量内存,它们很可能会绕过malloc()并直接分配和释放页面。


4

从技术角度来说

 p[0] = 1;
 p[1] = 2;

当您尝试使用悬空指针p时,会触发未定义行为(这意味着任何事情都可能发生)。

此外,即使是printf("%d\n",p);也会引发UB(printf()中格式说明符与参数类型不匹配)。


2
因此,在“main”中的两个printf行中都要考虑:第一个用于格式不匹配的字符串,第二个用于在参数中解除引用无效指针。 - R.. GitHub STOP HELPING ICE
@R:是的!我已经在我的帖子中添加了它。 - Prasoon Saurav

4

进行逻辑思考。

调用free(ptr)函数,意味着你告诉系统,之前由ptr所引用的内存现在可以被释放了。

也就是说,系统可以随意使用这块内存。相信我,迟早会有系统将自己的数据写入同一地址,覆盖你的数据,或者另一个多任务操作系统中的程序也可能做同样的事情。

你可能会问为什么ptr的值仍然相同?原因很简单:速度。 系统不知道你是否计划在free调用之后立即为ptr分配一个新的有效地址,或者你只是将其废弃不用。

无论如何,最好的做法是在free调用之后立即将ptr赋值为NULL指针:

free(ptr);
ptr = NULL;

因为在您的函数/模块/程序的另一部分,您将能够检查:

if(NULL == ptr){
/* ... */
}

顺便说一下,如果你在同一个地址上两次调用 free 函数,你的程序将崩溃 - 这是将 free 调用赋值为 NULL 的另一个好理由,因为 free(NULL) 是安全的操作:
free(ptr);
ptr = NULL; /* try to comment out/put back this line and see what happens */
free(ptr);

在复杂的程序中,这种情况可能会发生。

2

free()实际上是释放内存。但是,它不会对指针做任何操作。在C语言中,你可以尝试写入任何内存位置。除了段错误(如果尝试访问程序区域之外的内存,则会导致程序崩溃)以外,没有安全检查。然而,这并不意味着尝试使用已释放和/或未初始化的内存是一个好主意。那是一个内存错误。你会变得很讨厌它们。 :)


2
free的定义是将由malloc和相似函数分配的内存返回给系统。其实际发生的情况因不同的系统而异,可能有以下几种情况:
  1. 内存在内存分配器数据结构中被标记为“空闲”(您永远不会直接看到这些内容)。
  2. 内存被部分地覆盖了,被内存分配器数据所占用(其中一些会将内部数据存储在空闲块内)。
  3. 内存被重新分配给程序的其他部分——例如,printf可能会将某些内存用于某些内部目的,也可能不会使用——具体取决于实现。
  4. 内存被返回给操作系统,因此对进程不再可访问。
这些情况中的哪一种实际发生取决于C库的实现以及您调用free的时候系统的状态。但有一点应该清楚:您绝不能以任何方式再次使用已经通过free释放的内存。它可能会崩溃,也可能不会崩溃,但这都不是好的选择。
为了捕获这种情况——在free之后使用内存——有许多程序可供使用。在Linux中,最流行的工具是valgrind

你提醒我想到扫描标记垃圾回收。如果我两次释放p会发生什么? - slee
@slee 智能分配器(例如 glibc)可能会产生错误并中止程序。但如果分配器不那么智能,或者无法检测到情况,那么任何事情都可能发生——通常是坏的事情,例如内存损坏等。 - StasM

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