free 和 malloc 如何在 C 中工作?

How do free and malloc work in C?

提问人:user238082 提问时间:12/24/2009 最后编辑:Vijay Mathewuser238082 更新时间:6/9/2015 访问量:84866

问:

我试图弄清楚如果我尝试“从中间”释放指针会发生什么 例如,查看以下代码:

char *ptr = (char*)malloc(10*sizeof(char));

for (char i=0 ; i<10 ; ++i)
{
    ptr[i] = i+10;
}
++ptr;
++ptr;
++ptr;
++ptr;
free(ptr);

我收到崩溃,并显示未处理的异常错误消息。 我想了解 free 为什么以及如何工作,这样我不仅知道如何使用它,而且能够理解奇怪的错误和异常,并更好地调试我的代码ץ

多谢

c 内存 malloc

评论

1赞 GManNickG 12/24/2009
没有单一的“它是如何工作的”,因为它是特定于实现的。
9赞 paxdiablo 12/24/2009
@GMan小心,实现定义(意味着实现必须记录它并按照此行事)和未定义(这意味着任何事情都可能发生,包括猴子从你的屁股上飞出来)之间有真正的区别。:-)
1赞 GManNickG 12/24/2009
我的意思是“free()是如何工作的”,而不是“我的代码是做什么的?我正在回答标题问题。
1赞 R.. GitHub STOP HELPING ICE 2/26/2011
如果你提到猴子可以飞进去而不是飞出去,也许你会让那些不断提出UB问题的人听得更好。;-)
1赞 fredoverflow 9/29/2012
malloc() 和 free() 如何工作?

答:

7赞 sharptooth 12/24/2009 #1

这是未定义的行为 - 不要这样做。只有从 获得的指针,在此之前从不调整它们。free()malloc()

问题是必须非常快,所以它不会尝试找到你调整后的地址所属的分配,而是尝试将调整后的地址的块返回到堆中。这会导致未定义的行为 - 通常是堆损坏或程序崩溃。free()

评论

1赞 R.. GitHub STOP HELPING ICE 2/26/2011
我不会把这归类为速度快的问题。如果没有大量的簿记信息,这些信息也可能在内存方面花费很多,或者强加一个特别糟糕的设计,那么在给定一个随机指针的情况下,找到一个分配块的开始是根本不可能的。
0赞 Koray Tugay 6/4/2015
@R..“在给定一个随机指针的情况下,将分配块的开头输入进去根本不可能。”我不这么认为。。
8赞 PetrosB 12/24/2009 #2

从 http://opengroup.org/onlinepubs/007908775/xsh/free.html

free() 函数导致 ptr 指向的空间被解除分配;也就是说,可用于进一步分配。如果 ptr 为 null 指针,则不执行任何操作。否则,如果参数与之前由 calloc()、malloc()、realloc() 或 valloc() 函数返回的指针不匹配,或者如果通过调用 free() 或 realloc() 来释放空间,则行为未定义。 任何引用释放空间的指针的使用都会导致未定义的行为。

评论

0赞 GManNickG 12/24/2009
没有解释的链接并不是真正的答案。
1赞 PetrosB 12/24/2009
为什么!?我见过很多次,只有一个链接是被接受的答案!
8赞 paxdiablo 12/24/2009
链接、@Petros和其他人的问题可能与我不同意(很有可能看到我们有 120,000 多人),因为它们可能会消失(是的,甚至像维基百科这样的东西)。我不介意链接本身,但答案中应该有足够的内容,这样,即使互联网的其余部分被摧毁,SO 仍然有用。我倾向于做的是解释足够的内容来回答问题,然后为那些想要走得更远的人提供任何链接。
0赞 PetrosB 12/24/2009
实事求是地说,我不认为 Open Group 的网站会去任何地方。此外,还编辑了答案,并添加了一个不言自明的引用文本,该文本可以作为OP问题的答案。
15赞 Zeograd 12/24/2009 #3

大多数(如果不是全部)实现将在您正在操作的实际指针之前查找要释放几个字节的数据量。 进行百搭会导致内存映射损坏。free

如果你的示例,当你分配 10 个字节的内存时,系统实际上保留了 14 个字节。前 4 个包含您请求的数据量 (10),然后返回值 是指向分配的 14 个字节中第一个字节的未使用数据的指针。malloc

当您调用此指针时,系统将向后查找 4 个字节,以知道它最初分配了 14 个字节,以便知道要释放多少字节。此系统会阻止您提供要释放的数据量作为其自身的额外参数。freefree

当然,其他实现/可以选择其他方式来实现这一点。但是,它们通常不支持与 返回的指针或等效函数不同的指针。mallocfreefreemalloc

评论

0赞 onmyway133 2/20/2013
假设我有char s[3] = {a,b,c}。为什么 s == 'a' ??
1赞 Zeograd 3/15/2013
在这种特殊情况下,不涉及任何动态分配。编译器在堆栈上分配所需的 3 个字节,而不是在堆上分配。您不必(也不应该)免费呼叫
0赞 onmyway133 3/15/2013
你说“malloc 的返回值是指向分配的 14 个数据中未使用数据的第一个字节的指针”,但随后你说“向后查找 4 个字节”!??而且,它是否记录在某个地方?
1赞 Zeograd 3/20/2013
此信息取决于您使用的 malloc 实现,并且文档通常仅作为源代码中的注释找到。例如,在 GNU libc 实现中,您可以找到以下注释: 每个分配的块的最小开销:4 或 8 字节 每个错误块都有一个隐藏的开销字,其中包含大小和状态信息。
0赞 Eugene Shatsky 3/21/2019
@onmyway133 s 也是指向第一个数组元素的指针,但它只能偶然等于“a”字符。
127赞 Jason Williams 12/24/2009 #4

当你对一个块进行恶意定位时,它实际上分配的内存比你要求的要多一些。这个额外的内存用于存储信息,例如分配块的大小,以及指向块链中下一个可用/已用块的链接,有时还有一些“保护数据”,以帮助系统检测您是否写入了分配块的末尾。此外,大多数分配器会将总大小和/或内存部分的起始时间四舍五入为字节的倍数(例如,在 64 位系统上,它可能会将数据对齐为 64 位(8 字节)的倍数,因为对于处理器/总线来说,从未对齐地址访问数据可能更加困难和低效), 因此,您最终也可能会得到一些“填充”(未使用的字节)。

当您释放指针时,它会使用该地址来查找它添加到已分配块开头(通常)的特殊信息。如果你传入一个不同的地址,它将访问包含垃圾的内存,因此它的行为是未定义的(但最常见的是会导致崩溃)

稍后,如果你释放了块,但没有“忘记”你的指针,你可能会在将来意外地尝试通过该指针访问数据,并且行为是未定义的。可能会出现以下任何一种情况:

  • 内存可能被放在一个空闲块列表中,所以当你访问它时,它仍然恰好包含你留在那里的数据,并且你的代码正常运行。
  • 内存分配器可能已经将内存(部分)给了程序的另一部分,然后可能会覆盖(部分)旧数据,因此当您读取它时,您会得到垃圾,这可能会导致代码出现意外行为或崩溃。或者,您将覆盖其他数据,导致程序的另一部分在将来的某个时间点表现得很奇怪。
  • 内存可能已返回到操作系统(不再使用的内存“页”可以从地址空间中删除,因此该地址上不再有任何可用的内存 - 本质上是应用程序内存中未使用的“洞”)。当应用程序尝试访问数据时,将发生硬内存故障并终止进程。

这就是为什么确保在释放指针所指向的内存后不使用指针很重要的原因 - 最佳做法是在释放内存后将指针设置为 NULL,因为您可以轻松地测试 NULL,并且尝试通过 NULL 指针访问内存将导致错误但一致的行为, 这更容易调试。

评论

0赞 Tom Charles Zhang 1/27/2023
很好的解释,但是它仍然没有解释free()的实际工作原理。你基本上只不过是在说“C 库函数 void free(void *ptr) 释放了以前通过调用 calloc、malloc 或 realloc 分配的内存。
0赞 Tom Charles Zhang 1/27/2023
例如,我认为值得一提的是,在操作系统内部有某种“内存管理单元”,可以跟踪已分配和释放的内存块。当主机程序释放某些块时,操作系统可以自由地将这些内存块分配给其他程序。
0赞 Tom Charles Zhang 1/27/2023
“本质上是应用程序内存中未使用的'洞'”的部分在含义上也有点误导/模棱两可,没有完全解释特定进程的内存空间(或它的外观)。
1赞 Jason Williams 1/30/2023
@TomCharlesZhang:我的答案是概括的,因为 alloc/free 与操作系统交互的方式是一个实现细节,特定于代码所针对的C++变体、主机操作系统和硬件(CPU/内存)架构的组合。
6赞 Jason D 12/24/2009 #5

您释放了错误的地址。通过更改 ptr 的值,可以更改地址。free 无法知道它应该尝试释放从 4 个字节开始的块。保持原始指针完好无损,而不是纵的指针。正如其他人指出的那样,做你正在做的事情的结果是“不确定的”......因此,未处理的异常。

29赞 DigitalRoss 12/24/2009 #6

您可能知道您应该准确地传回您收到的指针。

因为 free() 一开始并不知道你的区块有多大,所以它需要辅助信息才能从其地址中识别原始区块,然后将其返回到自由列表中。它还将尝试将小的释放块与邻居合并,以产生更有价值的大自由块。

最终,分配器必须具有有关块的元数据,至少需要将长度存储在某处。

我将介绍三种方法。

  • 一个明显的地方是将其存储在返回的指针之前。它可以分配一个比请求的块大几个字节的块,将大小存储在第一个单词中,然后返回指向第二个单词的指针。

  • 另一种方法是保留一个单独的映射,至少描述分配块的长度,使用地址作为键。

  • 实现可以从地址派生一些信息,也可以从地图派生一些信息。4.3BSD 内核分配器(我认为称为“McKusick-Karel 分配器”)对小于页面大小的对象进行 2 次幂分配,并且只保留每页大小,使给定页面的所有分配都具有单一大小。

对于某些类型的第二种分配器,以及可能任何类型的第三种类型的分配器,实际上都可以检测到您已经推进了指针和 DTRT,尽管我怀疑是否有任何实现会为此烧毁运行时。

3赞 Jeet 2/17/2010 #7

永远不要这样做。

您释放了错误的地址。通过更改 ptr 的值,可以更改地址。free 无法知道它应该尝试释放从 4 个字节开始的块。保持原始指针完好无损,而不是纵的指针。正如其他人指出的那样,做你正在做的事情的结果是“不确定的”......因此出现未处理的异常

2赞 Koray Tugay 6/9/2015 #8

摘自《理解和使用 C 指针》一书

分配内存时,附加信息将作为堆管理器维护的数据结构的一部分进行存储。除其他外,此信息包括块的大小,并且通常紧挨着分配的块。