免费软件如何确定免费的程度?

513
在C编程中,您可以将任何类型的指针作为free函数的参数传递,那么它如何知道要释放的内存大小呢?当我将指针传递给某个函数时,我必须同时传递大小(例如,一个有10个元素的数组需要接收10作为参数以了解数组的大小),但是在free函数中不必传递大小。为什么不需要,并且我可以在自己的函数中使用这种技术来避免携带额外的数组长度变量吗?

一个类似的问题:https://dev59.com/MnRA5IYBdhLWcg3wuAYo(虽然我认为它不完全是重复的) - John Carter
“伙伴系统”是另一种方法,可以根据指针而无需在每个块中添加开销来进行。 - EvilTeach
这篇文章讲得很清楚:https://dev59.com/ZHI-5IYBdhLWcg3wO1vl - Zeeshan
11个回答

461
当你调用malloc()函数时,你需要指定分配内存的数量。实际使用的内存量略大于此值,并包含记录至少块大小的额外信息。你不能(可靠地)访问其他信息,也不应该这样做 :-)
当你调用free()函数时,它只需查看其他信息即可找出块的大小。

58
例如,BSD系统中有malloc_size()函数,可靠地从malloc()分配的指针中获取块大小。但是,没有可靠、可移植的方法实现这一点。 - laalto
72
重要的是要说明这个额外信息块位于返回指针之前。 - Georg Schölly
52
@gs 这取决于具体实现,但通常是在那里。 - Falaina
49
你能想象如果free()要求程序员准确报告malloc()块的大小会有多可怕吗?内存泄漏本来就已经够糟糕了。 - MusiGenesis
64
为什么malloc()和free()可以访问这些信息,但是你必须存储数组的大小?如果它们已经存储了这些信息,为什么不让它变得更容易,比如使用类似blockSize(ptr)这样的函数来获取块的大小? - corsiKa
显示剩余15条评论

198

C语言内存分配函数的大多数实现会为每个块存储记账信息,可以直接内联或单独存储。

一种典型的内联方式是实际上分配一个头部和你要求的内存,将其填充到某个最小大小。举个例子,如果你请求20字节,系统可能会分配一个48字节的块:

  • 16字节的头部包含大小、特殊标记、校验和、指向下一个/上一个块的指针等。
  • 32字节的数据区域(你的20字节填充到16的倍数)。

然后给你的地址是数据区域的地址。当你释放该块时,free只需要获取你提供的地址,并假设你没有破坏该地址或周围的内存,则会检查它之前的记账信息。以图形方式表示就是这样:

 ____ The allocated block ____
/                             \
+--------+--------------------+
| Header | Your data area ... |
+--------+--------------------+
          ^
          |
          +-- The address you are given

需要记住的是,头部和填充的大小完全由实现定义(实际上整个内存分配系统都是由实现定义的(a),但内联计算选项是常见的一种)。

内存分配信息中存在的校验和和特殊标记经常会导致错误,比如“内存区域已损坏”或“重复释放”,如果您覆盖它们或释放两次相同的内存。

填充(使分配更有效率)是为什么有时候您可以在请求的空间末尾写入一点内容而不会引起问题的原因(但是不要这样做,这是未定义行为,即使有时候有效,也不能这样做)。


(a)我曾在嵌入式系统中编写过malloc的实现,在那里无论您要求多少,都只得到128字节(那是系统中最大结构体的大小),假设您请求的是128字节或更少(请求更多会返回NULL值)。一个非常简单的位掩码(即不是内联的)用于决定是否分配了128字节的块。

我开发的其他内存分配系统针对16字节块、64字节块、256字节块和1K字节块具有不同的内存池,同样使用位掩码来决定哪些块被使用或可用。

这两个选项都成功减少了内存分配信息的开销,并增加了mallocfree的速度(在释放时无需合并相邻的块),这在我们工作的环境中尤为重要。


2
@user10678,malloc 的唯一真正要求是在成功的情况下为您提供至少与您请求的大小相同的内存块。单个块在访问其中的元素时是连续的,但并不要求这些块来自连续的区域。 - paxdiablo
1
相关问题:为什么没有malloc/free的变体,可以在释放内存时指定大小,这样就不必存储大小了? - user253751
3
@user253751,因为除了指针本身之外,还有一件更多的事情需要跟踪。这既是不必要的,也是危险的:void *x = malloc(200); free(x, 500); 不会有好结果 :-) 无论如何,为了效率,缓冲区的实际大小可能会更大(你不能依赖它)。 - paxdiablo
1
@paxdiablo 它还避免了浪费内存来保存大小。 - user253751
1
让系统记住大小比依赖开发者要好得多。要检查传递给free()的正确大小是不可能的。 - gnasher729
显示剩余2条评论

52

来自comp.lang.c的FAQ列表:free函数如何知道要释放的字节数?

malloc/free实现在分配内存块时记住每个块的大小,因此释放时不需要提供大小信息。(通常,大小信息存储在分配的内存块旁边,这也是为什么稍微超出分配内存块边界就会导致严重错误的原因)


8
这是一个非答案。问题确切地是:为什么Free能可靠地查找块的大小,但程序员没有可用的函数来执行此操作? - Bananach
1
这确实是malloc api的一个实现细节,没有标准的api可以以通用的方式获取此信息(据我所知)。"系统"记录它并在free上使用。也许答案不能令您满意,但我认为您不会得到更多通用适用的信息 :-) - jdehaan

9
该答案转自How does free() know how much memory to deallocate?,我被一个明显的重复问题突然禁止回答。对于malloc,堆分配器存储了原始返回指针与free释放内存所需的相关细节之间的映射关系。这通常涉及以分配器使用的任何形式存储内存区域的大小,例如原始大小、用于跟踪分配的二叉树中的节点或正在使用的内存“单元”的计数。如果您“重命名”指针或以任何方式复制它,则free不会失败。但是,它不是引用计数的,只有第一个free才是正确的。其他free将是“双重释放”错误。尝试free任何指向先前malloc未释放的值不同的指针都是错误的。无法部分释放从malloc返回的内存区域。

我改变了一个由malloc调用返回的指针的值,然后在没有错误的情况下释放了它。为什么?请查看这里:http://stackoverflow.com/questions/42618390/how-does-the-free-function-know-the-memory-size-to-free - smwikipedia

4

顺便提一下,GLib库有一些内存分配函数不会自动保存大小信息,因此您只需要将大小参数传递给free函数即可。这可以减少一部分开销。


3
最初的技术是分配一个略大的块并在开头存储大小,然后将剩余的部分交给应用程序。额外的空间保存了一个大小和可能链接到自由块的线程以便重复使用。
然而,这些技巧存在一些问题,如缓存和内存管理行为不佳。在块中正确使用内存往往会不必要地分页,并且会创建脏页面,从而使共享和写时复制变得复杂。
因此,更先进的技术是保持一个单独的目录。还开发了一些奇特的方法,其中内存区域使用相同的二次幂大小。
总的来说,答案是:分配一个单独的数据结构来保持状态。

通常在MacOS上,对于16字节、32字节、48字节等大小的malloc块,都使用大块内存。对于malloc/free,你只需要为每个块设置或清除一个位。非常友好地利用了缓存,因为128字节malloc块的位都在单个缓存行中。 - gnasher729
通常在MacOS上,你有大块用于16字节的分配,32字节的分配,48字节的分配等等。对于分配/释放内存,你只需为每个块设置或清除一个位。非常适合缓存,因为比如说128个分配块的位都在一个缓存行中。 - undefined

3
堆管理器在您调用malloc时将属于分配块的内存量存储在某处。我从未亲自实现过,但我猜分配块前面的内存可能包含元信息。

5
这是一种可能的实现方式,但是可以设计一个系统,在完全不同的页面中跟踪所有内存,并在与分配内存池不必接近的位置上维护单个表格。 - ephemient

3

malloc()free()是与系统/编译器有关的,因此很难给出具体答案。

更多信息请参见此其他问题


2
它们非常依赖于库(通常是C库,该库通常与操作系统紧密链接)。对于编译器来说,它们只是函数。 - Donal Fellows

2
为了回答你问题的后半部分:是的,你可以这样做,在C语言中一个相当常见的模式如下:
typedef struct {
    size_t numElements
    int elements[1]; /* but enough space malloced for numElements at runtime */
} IntArray_t;

#define SIZE 10
IntArray_t* myArray = malloc(sizeof(intArray_t) + SIZE * sizeof(int));
myArray->numElements = SIZE;

这是一种完全不同的技术,与BSD malloc用于小对象的技术不同(尽管它是创建Pascal样式数组的完美技术)。 - Pete Kirkham

1
回答第二个问题,是的,你可以(有点)使用与malloc()相同的技术,只需将每个数组内的第一个单元格分配给数组的大小即可。这样可以发送数组而无需发送额外的大小参数。

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