为什么C中的`free`没有占用要释放的字节数呢?

只是要清楚:我知道mallocfree是在C库中实现的,它通常从操作系统中分配大量内存,并进行自己的pipe理,将较小的内存分配给应用程序,并跟踪分配的字节。 这个问题不是如何免费知道自由多less 。

相反,我想知道为什么free这样摆在首位。 作为低级语言,我认为要求C程序员不仅要记住分配了什么内存,而且要记住多less(事实上,我通常会发现我最终跟踪了字节数无论如何都是)。 我也发现,明确地给出free字节数可能会允许一些性能优化,例如,具有用于不同分配大小的单独池的分配器将能够通过查看input参数来确定哪个池可以被释放,整体上将会有更less的空间开销。

所以,简而言之,为什么mallocfree创build的,因此需要内部跟踪分配的字节数? 这只是一个历史的意外?

一个小小的编辑:一些人提供了一些问题,如“如果你释放的数量与你分配的数量不同”。 我想象中的API可能只需要一个准确地释放分配的字节数; 释放或多或less可以简单地被UB或实现定义。 不过,我不想阻止关于其他可能性的讨论。

一个参数free(void *) (在Unix V7中引入)与前面提到的两个参数mfree(void *, size_t)相比还有另一个主要优势,我没有在这里提到过:一个参数free大大简化每个其他的 API与堆内存。 例如,如果free需要内存块的大小,那么strdup会以某种方式返回两个值(指针+大小)而不是一个(指针),而C使得多值返回比单值返回更麻烦。 而不是char *strdup(char *)我们必须写char *strdup(char *, size_t *) ,否则struct CharPWithSize { char *val; size_t size}; CharPWithSize strdup(char *) struct CharPWithSize { char *val; size_t size}; CharPWithSize strdup(char *) struct CharPWithSize { char *val; size_t size}; CharPWithSize strdup(char *) 。 (现在第二个选项看起来很诱人,因为我们知道以NUL结尾的string是“计算历史上最具灾难性的devise错误” ,但事后来看,在70年代,C将string作为简单char *实际上被认为是一个比Pascal和Algol等竞争对手更有优势的决定性优势 。)另外,这不仅仅是困扰这个问题 – 它会影响每个分配堆内存的系统或用户定义的函数。

早期的Unixdevise人员是非常聪明的人,而free的优于mfree有很多原因,所以基本上我认为问题的答案是他们注意到了这一点,并相应地devise了他们的系统。 我怀疑你会在他们做出这个决定的时刻,find他们脑海里发生的事情的直接logging。 但我们可以想象。

假装你用C编写应用程序,在V6 Unix上运行,使用双参数mfree 。 到目前为止,您的pipe理还算顺利,但随着程序变得越来越雄心勃勃 ,需要越来越多地使用堆分配的variables,跟踪这些指针大小变得越来越麻烦。 但是你有一个好主意:不是一直拷贝这些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); } 

而使用这些新function编写的代码越多,它们看起来就越棒。 它们不仅使您的代码更易于编写,而且还可以使您的代码更快速 – 两件事情经常不在一起! 在将这些size_t传递给所有的地方之前,这会增加复制的CPU开销,并且意味着你必须更频繁地溢出寄存器(特别是额外的函数参数)和浪费的内存(因为嵌套函数调用通常会导致size_t多个副本被存储在不同的堆栈帧中)。 在你的新系统中,你仍然需要花费内存来存储size_t ,但是只能存储一次,并且不会被复制到任何地方。 这些看起来可能效率很低,但请记住,我们正在谈论的是具有256 KiB内存的高端机器。

这让你快乐! 所以,你和那些在下一个Unix发行版中工作的大胡子男人分享你的酷招,但是这不会让他们开心,这让他们感到难过。 你看,他们只是在添加一些新的实用function,如strdup ,他们意识到,使用你的酷技巧的人将无法使用他们的新function,因为他们的新function都使用繁琐的指针+大小的API。 然后,这也让你感到难过,因为你意识到你必须在你编写的每个程序中自己重写好strdup(char *)函数,而不是能够使用系统版本。

可是等等! 这是1977年,向后兼容性将不会再发明5年! 而且,没有人认真地使用这个晦涩的“Unix”的东西,因为它的颜色不对。 K&R的第一个版本现在正在向出版商的方向发展,但这没什么问题 – 它在第一页上说“C不提供直接处理string等组合对象的操作……没有堆…“。 在历史的这个时候, string.hmalloc是供应商扩展(!)。 所以,build议大胡子#1,我们可以改变他们,但我们喜欢; 为什么我们不只是宣布你的棘手的分配器是官方分配器?

几天之后,大胡子#2看到了新的API,并说,嘿,等等,这比以前好,但它仍然花费整个字每个分配存储大小。 他认为这是亵渎的下一个东西。 其他人都像他疯了一样看着他,因为你还能做什么? 那天晚上,他迟到了,发明了一个新的分配器,它根本不存储大小,而是通过在指针值上执行黑魔法位移来推断它,并交换它,同时保持新的API。 新的API意味着没有人注意到开关,但他们注意到第二天早上编译器使用的内存减less了10%。

现在大家都开心了:你的代码更容易编写,速度更快,大胡子#1可以写出一个很好的简单步骤,人们可以使用,大胡子的人#2 – 相信他已经赢得了他的保留了一下- 回到乱糟糟的quines 。 装运它!

或者至less,这是如何发生的。

“为什么C中的free不能释放字节数呢?”

因为没有必要,反正也不太合理

当你分配一些东西时,你想告诉系统有多less字节分配(显而易见的原因)。

但是,如果您已经分配了对象,则现在确定返回的内存区域的大小。 这是隐含的。 这是一块连续的记忆。 你不能释放它的一部分(让我们忘记realloc() ,这不是它正在做什么),你只能释放整个事情。 你也不能“释放X字节” – 你可以从malloc()获得内存块,或者你不需要。

而现在,如果你想释放它,你可以告诉内存pipe理器系统:“这是这个指针, free()它指向的块。 – 内存pipe理器将知道如何做,因为它隐式知道大小,或者因为它甚至可能不需要大小。

例如, malloc()大多数典型实现都维护一个链接到指向空闲和分配内存块的指针列表。 如果你传递一个指向free()的指针,它只会在“已分配”列表中search该指针,取消链接相应的节点并将其附加到“空闲”列表中。 它甚至不需要区域大小。 当它可能试图重新使用该块时,它只需要这个信息。

C可能不像C ++那样“抽象”,但它仍然是一个对汇编的抽象。 为此,最低层次的细节被拿出来。 这样做可以防止大部分的alignment和填充,这将使所有的C程序不可移植。

总而言之, 这是写作抽象的整个观点

实际上,在古老的Unix内核内存分配器中, mfree()取得了一个size参数。 malloc()mfree()保存了两个数组(一个用于核心内存,另一个用于交换),其中包含空闲块地址和大小的信息。

在Unix V6之前没有用户空间分配器(程序只使用sbrk() )。 在Unix V6中,iolib包含一个alloc(size)和一个free()调用的分配器,它不带大小参数。 每个内存块都有它的大小和一个指向下一个块的指针。 指针只在空闲块上使用,当空闲列表行进时,被用作在使用块中的块存储器。

在Unix 32V和Unix V7中,这被replace为一个新的malloc()free()实现,其中free()没有使用size参数。 实现是一个循环列表,每个块前面都有一个包含指向下一个块的指针的字,以及一个“忙”(已分配)位。 所以, malloc()/free()甚至没有跟踪显式大小。

为什么C中的free不会占用要释放的字节数呢?

因为它不需要。 这些信息已经在由malloc / free执行的内部pipe理中可用。

以下是两个考虑因素(可能会或可能不会影响这个决定):

  • 为什么你会期望一个函数接收一个它不需要的参数?

    (这将使几乎所有依赖dynamic内存的客户端代码复杂化,并为应用程序添加完全不必要的冗余)。 跟踪指针分配已经是一个难题。 跟踪内存分配以及相关大小会不必要地增加客户端代码的复杂性。

  • 在这些情况下,改变的free函数会做什么?

     void * p = malloc(20); free(p, 25); // (1) wrong size provided by client code free(NULL, 10); // (2) generic argument mismatch 

    它会不会自由 (导致内存泄漏?)? 忽略第二个参数? 通过调用exit来停止应用程序? 实现这一点会在你的应用程序中增加额外的失败点,对于你可能不需要的function(如果你需要的话,请看下面的最后一点 – “在应用程序级别实现解决scheme”)。

相反,我想知道为什么自由这样摆在首位。

因为这是做到这一点的“正确”方式。 一个API应该需要它需要的参数来执行它的操作, 并且不会超过这个

我也发现,明确地给出空闲字节数可能会允许一些性能优化,例如,具有用于不同分配大小的单独池的分配器将能够通过查看input参数来确定哪个池可以被释放,整体上将会有更less的空间开销。

正确的实施方式是:

  • (在系统级)在malloc的实现中 – 没有任何东西阻止库实现者写malloc根据接收的大小在内部使用各种策略。

  • (在应用程序级别),通过包装malloc并在您自己的API中免费,并使用它们(在您的应用程序中可能需要的任何地方)。

记住五大理由:

  1. 很方便。 它消除了程序员的一大堆开销,避免了一类非常难以跟踪的错误。

  2. 它打开了释放一部分块的可能性。 但是,由于memory management者通常想要追踪信息,所以不清楚这意味着什么?

  3. 轨道上的亮度比赛是关于填充和alignment的地方。 内存pipe理的本质意味着分配的实际大小可能与请求的大小相差很大。 这意味着,可以free地要求一个大小以及一个位置malloc将不得不改变,以返回分配的实际大小。

  4. 无论如何,目前尚不清楚是否有任何实际的好处。 一个典型的内存pipe理器对每个内存块都有4到16个字节的头部,包括大小。 这个块头对于分配的和未分配的内存可能是常见的,并且当相邻的块被释放时,它们可以一起被折叠。 如果你让调用者存储空闲内存,你可以释放大概4个字节的每个块,因为在分配的内存中没有单独的大小字段,但是由于调用者需要将其存储在某个地方,所以该大小字段可能不会被获得。 但是现在,这些信息分散在内存中,而不是可预见地位于标题块中,无论如何这可能不太有效。

  5. 即使效率更高,你的程序也不太可能花费大量的时间来释放内存所以好处很小。

顺便提一下,如果没有这些信息(您可以使用地址来确定分配的位置),那么您可以轻松实现针对不同大小项目的单独分配器的想法。 这是通常在C ++中完成的。

稍后添加

另一个可笑的回答是,提出了std :: allocator来certificatefree可以这样工作,但事实上, free就是这样工作的一个很好的例子。 malloc / free和std :: allocator之间有两个主要区别。 首先, mallocfree是面向用户的 – 它们是为一般的程序员而devise的,而std::allocator则是为标准库提供专门的内存分配。 这提供了一个很好的例子,当我的第一个观点没有或者不重要的时候。 由于它是一个图书馆,无论如何,处理追踪大小的复杂性的困难对用户是隐藏的。

其次,std :: allocator 总是与相同大小的项目一起工作,这意味着它有可能使用最初传递的元素数来确定有多less空闲。 为什么这不同于free本身就是说明性的。 在std::allocator中,要分配的项目总是相同的,已知的,大小相同的项目,因此它们始终具有相同的alignment要求。 这意味着分配器可以被专门用来在开始时简单地分配这些项目的数组,并根据需要进行分配。 你不能free这样做,因为没有办法保证返回的最佳大小是要求的大小,而是有时返回比调用者要求的更大的块要高效得多,因此用户或经理需要跟踪实际授予的确切大小。 将这些实现细节传递给用户是一个不必要的麻烦,不会给调用者带来任何好处。

– *如果有人仍然很难理解这一点,考虑这一点:一个典型的内存分配器添加less量的跟踪信息到一个内存块的开始,然后返回一个指针偏移量。 例如,这里存储的信息通常包括指向下一个空闲块的指针。 假设这个头只有4个字节(实际上比大多数真实的库小),并且不包含大小,那么假设我们有一个20字节的空闲块,当用户请求一个16字节的块时,系统将返回16字节块,但留下一个永远不会被使用的4byte片段,每次malloc被调用时都会浪费时间。 如果pipe理员只是简单地返回20个字节的数据块,那么它就可以保存这些混乱的碎片,并且能够更加干净地分配可用的内存。 但是,如果系统要正确地做到这一点,而不跟踪大小本身,那么我们就要求用户跟踪 – 对于每一个单独的分配 – 实际分配的内存量,如果它是免费传回的话。 相同的参数适用于与所需边界不匹配的types/分配的填充。 因此,要求free地采取一个大小是(a)完全无用的,因为存储器分配器不能依靠传递的大小来匹配实际分配的大小,或者(b)毫无意义地要求用户做工作跟踪真实大小可以由任何明智的内存pipe理器轻松处理。

我只是把这个作为回答,不是因为它是你所希望的,而是因为我相信这是唯一正确的答案:

这本来可能被认为很方便,而且之后也不能改进。
有可能没有令人信服的理由。 (但如果显示不正确,我会很乐意删除这个)

如果可能的话, 有好处:你可以预先分配一个你知道的大小的内存,然后每次释放一点点 – 而不是重复分配和释放小块内存。 目前这样的任务是不可能的。


对于那些认为通过这个规模的人来说, 很多 (很多)是非常荒谬的:

我可以把你引用到C ++的std::allocator<T>::deallocate方法的devise决定吗?

 void deallocate(pointer p, size_type n); 

p指向的区域内的所有n T物体都应该在这个调用之前销毁。
n应该匹配传递给allocate的值来获得这个内存。

我想你会有一个相当“有趣”的时间来分析这个devise决定。


至于operator delete ,事实certificate,2013年N3778提案(“C ++ Sized Deallocation”)也是为了解决这个问题。


1只要看看原始问题下的评论,看看有多less人做出了草率的断言,例如“问大小对于free电话是完全没用的”,以certificate缺lesssize参数。

malloc和free一起携手,每个“malloc”由一个“free”匹配。 因此,总而言之,与以前的“malloc”相匹配的“自由”匹配应该简单地释放malloc分配的内存量 – 这是99%的情况下最有用的多数情况。 想象一下,如果全世界所有程序员使用malloc / free的所有内存错误,都需要程序员跟踪malloc中分配的数量,然后记住释放相同的内存错误。 你所说的场景应该是在某种内存pipe理实现中使用多个malloc / frees。

我build议这样做是因为这样非常方便,不需要手动跟踪大小信息(在某些情况下),也不太容易出现程序员错误。

此外,realloc将需要这种簿记信息,我预计不仅包含分配大小。 即它允许它的工作机制被定义。

你可以编写你自己的分配器,它可以按照你所build议的方式工作,而且它通常以c ++为池分配器完成,对于特定的情况类似(可能有巨大的性能增益),尽pipe这通常是以操作符新分配池块。

我不明白分配器如何工作,不跟踪分配的大小。 如果不这样做,它将如何知道哪个内存可用于满足未来的malloc请求? 它必须至less存储某种包含地址和长度的数据结构,以指示可用内存块的位置。 (当然,存储空闲空间列表相当于存储已分配空间的列表)。

那么,你唯一需要的是一个指针,你将用它来释放你之前分配的内存。 字节的数量由操作系统pipe理,所以你不必担心它。 没有必要获得由free()返回的分配的字节数。 我build议你用一种手动的方式来计算正在运行的程序分配的字节数/位置:

If you work in Linux and you want to know the amount of bytes/positions malloc has allocated, you can make a simple program that uses malloc once or n times and prints out the pointers you get. In addition, you must make the program sleep for a few seconds (enough for you to do the following). After that, run that program, look for its PID, write cd /proc/process_PID and just type "cat maps". The output will show you, in one specific line, both the beginning and the final memory addresses of the heap memory region (the one in which you are allocating memory dinamically).If you print out the pointers to these memory regions being allocated, you can guess how much memory you have allocated.

希望能帮助到你!

为什么要这样? malloc() and free() are intentionally very simple memory management primitives , and higher-level memory management in C is largely up to the developer. Ť

Moreover realloc() does that already – if you reduce the allocation in realloc() is it will not move the data, and the pointer returned will be the the same as the original.

It is generally true of the entire standard library that it is composed of simple primitives from which you can build more complex functions to suit your application needs. So the answer to any question of the form "why does the standard library not do X" is because it cannot do everything a programmer might think of (that's what programmers are for), so it chooses to do very little – build your own or use third-party libraries. If you want a more extensive standard library – including more flexible memory management, then C++ may be the answer.

You tagged the question C++ as well as C, and if C++ is what you are using, then you should hardly be using malloc/free in any case – apart from new/delete, STL container classes manage memory automatically, and in a manner likely to be specifically appropriate to the nature of the various containers.