明确一点:我知道 malloc
和 free
是在 C 库中实现的,它通常从操作系统分配内存块并自行管理以将较小的内存分配给应用程序并跟踪分配的字节数。这个问题不是How does free know how much to free。
相反,我想知道为什么 free
首先是这样制作的。作为一种低级语言,我认为要求 C 程序员不仅要跟踪分配了哪些内存,还要跟踪分配了多少内存是完全合理的(事实上,我通常发现我最终会跟踪字节数无论如何分配)。我还想到,明确地将字节数提供给 free
可能会允许一些性能优化,例如,具有不同分配大小的单独池的分配器将能够仅通过查看输入来确定要释放哪个池论点,并且总体上会减少空间开销。
所以,简而言之,为什么要创建 malloc
和 free
以使它们需要在内部跟踪分配的字节数?这只是历史偶然吗?
一个小编辑:一些人提供了诸如“如果您释放的数量与分配的数量不同怎么办”之类的观点。我想象中的 API 可能只需要一个来准确释放分配的字节数;释放或多或少可以简单地由 UB 或实现定义。不过,我不想阻止讨论其他可能性。
malloc
的调用者不知道返回的块的大小。 malloc
通常会返回一个大于请求的块。充其量,程序员可以传入 malloc()
调用中请求的大小,这对 free()
的实现者毫无帮助。
单参数 free(void *)
(在 Unix V7 中引入)与我在这里没有提到的之前的双参数 mfree(void *, size_t)
相比有另一个主要优势:一个参数 free
极大地简化了每个其他使用堆内存的 API。例如,如果 free
需要内存块的大小,那么 strdup
将不得不返回两个值(指针 + 大小)而不是一个(指针),并且 C 使多值返回比单个返回更麻烦-值返回。我们必须写 char *strdup(char *, size_t *)
或 struct CharPWithSize { char *val; size_t size}; CharPWithSize strdup(char *)
而不是 char *strdup(char *)
。 (现在第二个选项看起来很诱人,因为我们知道 NUL 终止的字符串是 "most catastrophic design bug in the history of computing",但这是事后诸葛亮。早在 70 年代,C 将字符串处理为简单 char *
的能力实际上被认为是 {2 }.) 另外,受此问题影响的不仅仅是 strdup
—— 它会影响每个分配堆内存的系统或用户定义的函数。
早期的 Unix 设计者是非常聪明的人,free
优于 mfree
的原因有很多,所以基本上我认为问题的答案是他们注意到了这一点并相应地设计了他们的系统。我怀疑你会在他们做出决定的那一刻找到任何关于他们头脑中发生的事情的直接记录。但我们可以想象。
假设您正在用 C 语言编写应用程序以在 V6 Unix 上运行,并带有两个参数 mfree
。到目前为止,您的管理还不错,但是随着您的程序 become more ambitious 并且需要越来越多地使用堆分配的变量,跟踪这些指针大小变得越来越麻烦。但是您有一个绝妙的主意:您可以编写一些实用函数,将大小直接存储在分配的内存中,而不是一直复制这些 size_t
:
void *my_alloc(size_t size) {
void *block = malloc(sizeof(size) + size);
*(size_t *)block = size;
return (void *) ((size_t *)block + 1);
}
void my_free(void *block) {
block = (size_t *)block - 1;
mfree(block, *(size_t *)block);
}
你使用这些新函数编写的代码越多,它们看起来就越棒。它们不仅使您的代码更易于编写,而且还使您的代码更快 -- 这两件事通常不会一起出现!在您到处传递这些 size_t
之前,这增加了复制的 CPU 开销,并且意味着您必须更频繁地溢出寄存器(尤其是对于额外的函数参数),并浪费内存(因为嵌套函数调用通常会导致 size_t
的多个副本存储在不同的堆栈帧中)。在您的新系统中,您仍然需要花费内存来存储 size_t
,但只需要一次,而且它永远不会被复制到任何地方。这些可能看起来效率很低,但请记住,我们谈论的是具有 256 KiB RAM 的高端机器。
这让你开心!因此,您与正在开发下一个 Unix 版本的大胡子男人分享您的酷炫技巧,但这不会让他们高兴,只会让他们难过。你看,他们只是在添加一堆像 strdup
这样的新实用程序函数,他们意识到使用你的酷技巧的人将无法使用他们的新函数,因为他们的新函数都使用了繁琐的指针+大小 API。然后这也让你感到难过,因为你意识到你必须在你编写的每个程序中自己重写好的 strdup(char *)
函数,而不是能够使用系统版本。
可是等等!这是 1977 年,向后兼容性要再过 5 年才会发明!此外,实际上没有人真正使用这个晦涩难懂的“Unix”东西,它的名字很淡。 K&R 的第一版现在正在送达出版商,但这没问题——它在第一页上说“C 不提供直接处理诸如字符串之类的复合对象的操作......没有堆……”。在历史的这一点上,string.h
和 malloc
是供应商扩展 (!)。因此,Bearded Man #1 建议,我们可以随心所欲地更改它们;我们为什么不直接将您的棘手分配器声明为 official 分配器?
几天后,Bearded Man #2 看到新的 API 并说嘿,等等,这比以前好,但它仍然在每个分配中花费一个完整的单词来存储大小。他认为这是亵渎神明的下一件事情。其他人都看着他,就像他疯了一样,因为你还能做什么?那天晚上他熬夜并发明了一个新的分配器,它根本不存储大小,而是通过对指针值执行黑魔法位移来动态推断它,并在保持新 API 的同时将其交换。新的 API 意味着没有人注意到切换,但他们确实注意到第二天早上编译器使用的 RAM 减少了 10%。
现在每个人都很高兴:您获得了更容易编写和更快的代码,Bearded Man #1 可以编写一个很好的简单 strdup
供人们实际使用,而 Bearded Man #2 - 相信他已经赢得了他的保留有点——回到messing around with quines。装运它!
或者至少,这就是它可能发生的方式。
“为什么 C 中的 free 不占用要释放的字节数?”
因为没有必要,而且无论如何也没有什么意义。
当你分配一些东西时,你想告诉系统要分配多少字节(出于显而易见的原因)。
但是,当您已经分配了对象时,您返回的内存区域的大小现在已确定。这是隐含的。它是一个连续的内存块。你不能释放它的一部分(让我们忘记 realloc()
,反正它不是这样做的),你只能释放整个东西。 strong> 您也不能“释放 X 字节”——您要么释放从 malloc()
获得的内存块,要么不释放。
现在,如果你想释放它,你可以告诉内存管理系统:“这是这个指针,free()
它指向的块。” - 内存管理器会知道如何做到这一点,要么因为它隐含地知道大小,要么因为它甚至可能不需要大小。
例如,malloc()
的大多数典型实现都维护一个指向空闲和已分配内存块的指针链表。如果您将指针传递给 free()
,它只会在“已分配”列表中搜索该指针,取消链接相应节点并将其附加到“空闲”列表中。 它甚至不需要区域大小。它仅在它可能尝试重新使用有问题的块时才需要该信息。
实际上,在古老的 Unix 内核内存分配器中,mfree()
采用了 size
参数。 malloc()
和 mfree()
保留了两个数组(一个用于核心内存,另一个用于交换),其中包含有关空闲块地址和大小的信息。
在 Unix V6 之前没有用户空间分配器(程序只使用 sbrk()
)。在 Unix V6 中,iolib 包含一个带有 alloc(size)
的分配器和一个不带大小参数的 free()
调用。每个内存块前面都有它的大小和指向下一个块的指针。指针仅在空闲块上使用,在遍历空闲列表时,并在使用中的块上被重用为块内存。
在 Unix 32V 和 Unix V7 中,这被一个新的 malloc()
和 free()
实现所取代,其中 free()
没有使用 size
参数。该实现是一个循环列表,每个块前面都有一个单词,该单词包含指向下一个块的指针和一个“忙碌”(已分配)位。因此,malloc()/free()
甚至没有记录明确的大小。
C 可能不像 C++ 那样“抽象”,但它仍然是一种对汇编的抽象。为此,最底层的细节被排除在外。这可以防止您在大多数情况下不必担心对齐和填充,这会使您的所有 C 程序都不可移植。
简而言之,这就是编写抽象的全部意义所在。
malloc
询问 N 个字节,它返回一个指向整个页面开头的指针(因为 对齐、填充或其他约束,则无法用户跟踪它 - 强迫他们这样做会适得其反。
malloc
总是可以简单地返回一个对齐的指针,而无需存储分配的大小。 free
然后可以四舍五入到适当的对齐,以确保正确释放所有内容。我看不出问题出在哪里。
size
参数传递给 free
会导致另一个错误来源。
为什么 C 中的 free 不占用要释放的字节数?
因为不需要。这些信息已经在 malloc/free 执行的内部管理中可用。
以下是两个考虑因素(可能会或可能不会促成此决定):
为什么你会期望一个函数接收它不需要的参数? (这会使几乎所有依赖于动态内存的客户端代码复杂化,并为您的应用程序添加完全不必要的冗余)。跟踪指针分配已经是一个难题。跟踪内存分配以及相关大小会不必要地增加客户端代码的复杂性。
在这些情况下,改变后的自由函数会做什么?无效 * p = malloc(20);免费(第 25 页); // (1) 客户端代码提供的错误大小 free(NULL, 10); // (2) 泛型参数不匹配 它不会释放(导致内存泄漏?)?忽略第二个参数?通过调用 exit 来停止应用程序?实现这一点会在您的应用程序中添加额外的故障点,对于您可能不需要的功能(如果您需要它,请参阅下面的最后一点 - “在应用程序级别实施解决方案”)。
相反,我想知道为什么首先以这种方式制作免费。
因为这是做到这一点的“正确”方式。 API 应该需要执行其操作所需的参数,仅此而已。
我还想到,明确给出要释放的字节数可能会允许一些性能优化,例如,具有不同分配大小的单独池的分配器将能够仅通过查看输入参数来确定要从哪个池中释放,并且总体上会减少空间开销。
实现它的正确方法是:
(在系统级别)在 malloc 的实现中 - 没有什么可以阻止库实现者根据收到的大小编写 malloc 以在内部使用各种策略。
(在应用程序级别)通过将 malloc 和 free 包装在您自己的 API 中,并改为使用它们(在您可能需要的应用程序中的任何地方)。
五个原因浮现在脑海:
这很方便。它消除了程序员的全部开销,并避免了一类极难跟踪的错误。它开辟了释放部分块的可能性。但是由于内存管理器通常想要跟踪信息,所以不清楚这意味着什么? Lightness Races In Orbit 是关于填充和对齐的。内存管理的性质意味着分配的实际大小很可能与您要求的大小不同。这意味着可以自由地要求大小以及位置 malloc 必须更改以返回实际分配的大小。无论如何,尚不清楚传递大小是否有任何实际好处。典型的内存管理器对于每个内存块有 4-16 字节的标头,其中包括大小。这个块头对于已分配和未分配的内存是通用的,当相邻的块空闲时,它们可以折叠在一起。如果您让调用者存储空闲内存,您可以通过在分配的内存中没有单独的大小字段来释放每个块大约 4 个字节,但是由于调用者需要将其存储在某个地方,因此该大小字段可能无论如何都不会获得。但是现在这些信息分散在内存中,而不是可预测地位于标头块中,无论如何这可能会降低操作效率。即使它更有效,您的程序也不太可能花费大量时间来释放内存,因此好处会很小。
顺便说一句,如果没有这些信息,您关于不同大小项目的单独分配器的想法很容易实现(您可以使用地址来确定分配发生的位置)。这通常在 C++ 中完成。
稍后添加
另一个相当荒谬的答案提出了 std::allocator 作为 free
可以以这种方式工作的证据,但事实上,它是一个很好的例子,说明为什么 free
不能以这种方式工作。 malloc
/free
的作用与 std::allocator 的作用有两个主要区别。首先,malloc
和 free
是面向用户的 - 它们是为普通程序员设计的 - 而 std::allocator
旨在为标准库提供专业的内存分配。这提供了一个很好的例子,说明我的第一个观点何时不重要或不重要。由于它是一个库,因此无论如何处理跟踪大小的复杂性的困难对用户来说都是隐藏的。
其次,std::allocator 总是与相同大小的项目一起工作,这意味着它可以使用最初传递的元素数量来确定有多少空闲。为什么这与 free
本身不同是说明性的。在 std::allocator
中,要分配的项目始终具有相同的已知大小,并且始终具有相同的项目类型,因此它们始终具有相同的对齐要求。这意味着分配器可以专门用于在开始时简单地分配这些项目的数组并根据需要将它们分配出去。您不能使用 free
执行此操作,因为无法保证返回的最佳大小是要求的大小,相反,有时返回比调用者要求的更大的块更有效*,因此 要么用户或经理需要跟踪实际授予的准确大小。将这些类型的实现细节传递给用户是一个不必要的头痛,对调用者没有任何好处。
-* 如果有人仍然难以理解这一点,请考虑一下:典型的内存分配器将少量跟踪信息添加到内存块的开头,然后返回一个指针偏移量。例如,此处存储的信息通常包括指向下一个空闲块的指针。假设标头只有 4 个字节长(实际上比大多数真实库小),并且不包括大小,然后想象我们有一个 20 字节的空闲块,当用户要求一个 16 字节的块时,天真系统会返回 16 字节的块,但会留下一个 4 字节的片段,每次调用 malloc
时都不会浪费时间。相反,如果管理器只是返回 20 字节块,那么它可以避免这些杂乱的碎片堆积,并且能够更干净地分配可用内存。但是,如果系统要在不跟踪大小本身的情况下正确执行此操作,我们则要求用户跟踪 - 对于每一次分配 - 如果要将其免费传回,则 实际 分配的内存量.相同的论点适用于与所需边界不匹配的类型/分配的填充。因此,最多要求 free
获取大小要么是(a)完全没有用,因为内存分配器不能依赖传递的大小来匹配实际分配的大小,要么(b)毫无意义地要求用户进行工作跟踪任何明智的内存管理器都可以轻松处理的 real 大小。
free
调用要正确传递要释放的大小,它必须知道这一点。
我只是将其发布为答案,不是因为它是您希望的答案,而是因为我相信这是唯一看似正确的答案:
大概是本来就觉得方便,后来就没法改进了。可能没有令人信服的理由。 (但如果显示不正确,我会很乐意删除它。)
如果可能的话会有好处:您可以分配一块您事先知道其大小的大块内存,然后一次释放一点点——而不是反复分配和释放小块内存。目前这样的任务是不可能的。
对于许多(许多人!)认为传递尺寸是如此荒谬的人:
我可以向您推荐 C++ 对 std::allocator
void deallocate(pointer p, size_type n);
p 指向的区域中的所有 n T 个对象都应在此调用之前销毁。 n 应匹配传递给 allocate 以获得此内存的值。
我认为您将有一个相当“有趣”的时间来分析这个设计决策。
至于 operator delete,事实证明 2013 N3778 提案(“C++ Sized Deallocation”)也打算解决这个问题。
1只需看看原始问题下的评论,看看有多少人草率断言,例如“要求的尺寸对于 free
调用完全没用”证明缺少 size
参数的合理性。
malloc()
实现,它还无需记住关于已分配区域的任何内容,从而将分配开销减少到对齐开销。 malloc()
将能够自己在已释放的块中进行所有簿记。在某些用例中,这将是一个很大的改进。不鼓励在几个小块中释放一大块内存,但这会大大增加碎片。
std::allocator
仅 分配特定的已知大小的元素。它不是一个通用的分配器,比较是苹果和橘子。
malloc 和 free 齐头并进,每个“malloc”都与一个“free”相匹配。因此,与先前的“malloc”匹配的“free”应该简单地释放由该 malloc 分配的内存量是完全有意义的——这是在 99% 的情况下有意义的大多数用例。想象一下,如果全世界所有程序员都使用 malloc/free,那么所有的内存错误都需要程序员跟踪 malloc 中分配的数量,然后记得释放它们。您谈论的场景实际上应该是在某种内存管理实现中使用多个 malloc/frees。
gets
、printf
、手动循环(逐个)、未定义的行为、格式字符串、隐式等其他错误工厂时,“想象所有 [...] 错误”是没有意义的转换,位技巧等。
我建议这样做是因为不必以这种方式(在某些情况下)手动跟踪大小信息非常方便,而且不太容易出现程序员错误。
此外,realloc 需要这个簿记信息,我希望它不仅仅包含分配大小。即,它允许实现定义其工作的机制。
您可以编写自己的分配器,尽管它以您建议的方式工作,并且通常在 c++ 中以针对特定情况的类似方式为池分配器完成(具有潜在的巨大性能提升),尽管这通常是根据运算符实现的new 用于分配池块。
我看不到不跟踪其分配大小的分配器将如何工作。如果它不这样做,它如何知道哪块内存可用于满足未来的 malloc
请求?它至少必须存储某种包含地址和长度的数据结构,以指示可用内存块的位置。 (当然,存储可用空间列表等同于存储已分配空间列表)。
好吧,您唯一需要的是一个指针,您将使用它来释放您之前分配的内存。字节数由操作系统管理,因此您不必担心。没有必要获取 free() 返回的分配的字节数。我建议您手动计算正在运行的程序分配的字节数/位置数:
如果您在 Linux 中工作并且想知道 malloc 已分配的字节/位置的数量,您可以编写一个使用 malloc 一次或 n 次并打印出您获得的指针的简单程序。此外,您必须让程序休眠几秒钟(足以让您执行以下操作)。之后,运行该程序,查找它的 PID,写入 cd /proc/process_PID 并输入“cat maps”。输出将在一个特定的行中向您显示堆内存区域的开始和最终内存地址(您正在动态分配内存的那个)。如果您打印出指向这些正在分配的内存区域的指针,您可以猜出你分配了多少内存。
希望能帮助到你!
为什么要呢? malloc() 和 free() 是故意非常简单的内存管理原语,C 中更高级别的内存管理很大程度上取决于开发人员。吨
此外 realloc() 已经这样做了 - 如果你减少 realloc() 中的分配,它不会移动数据,并且返回的指针将与原始指针相同。
整个标准库通常是由简单的原语组成的,您可以从中构建更复杂的功能以满足您的应用程序需求。因此,对于“为什么标准库不做 X”形式的任何问题的答案是因为它不能做程序员可能想到的所有事情(这就是程序员要做的事情),所以它选择做的很少——构建你自己的或使用第三方库。如果你想要一个更广泛的标准库——包括更灵活的内存管理,那么 C++ 可能是答案。
您标记了 C++ 和 C 的问题,如果您使用的是 C++,那么无论如何您都不应该使用 malloc/free - 除了 new/delete,STL 容器类会自动管理内存,并且可能以某种方式特别适合各种容器的性质。
realloc
绝对允许移动数据。根据 C 标准,将 realloc
实现为 malloc
+memcpy
+free
是完全合法的。并且有充分的理由说明实现可能希望移动已减小大小的分配,例如避免内存碎片。