malloc和calloc有什么不同?

942

做以下两种操作有何区别:

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

并且:

ptr = calloc(MAXELEMS, sizeof(char*));

何时使用callocmalloc更好,反之亦然?


48
在C语言中,你不需要给malloc系列函数的返回值进行类型转换。 - phuclv
9
在C语言中,你可以更加通用地写作:ptr = calloc(MAXELEMS, sizeof(*ptr));。其中,calloc用于动态分配内存,MAXELEMS表示需要分配的元素个数,sizeof(*ptr)表示每个元素所占内存的大小,ptr表示指向分配内存的指针。 - chqrlie
8
一篇有趣的帖子,讲述了calloc和malloc+memset之间的区别。https://vorpus.org/blog/why-does-calloc-exist/ - ddddavidee
2
@ddddavidee 我也是在对网络上那么多答案不满意之后才发现了那篇博客。Nathaniel J. Smith 对于他的分析应该获得100多个 Stack Overflow 积分。 - lifebalance
14个回答

1005

calloc() 会为您提供一个零初始化的缓冲区,而 malloc() 则会保留内存未初始化。

对于大型分配,大多数主流操作系统下的 calloc 实现将从操作系统获取已知的零值页面(例如通过 POSIX 的 mmap(MAP_ANONYMOUS) 或 Windows 的 VirtualAlloc),因此它不需要在用户空间中编写它们。这也是正常的 malloc 从操作系统获取更多页面的方式;calloc 只是利用了操作系统的保证。

这意味着 calloc 内存仍然可以是“干净”的和惰性分配的,并且可以复制到全局共享的物理零页面上。(假设具有虚拟内存的系统。)例如,在 Linux 上进行 性能实验 就可以看到这种效果。

一些编译器甚至可以为您优化 malloc + memset(0) 以转换为 calloc,但最好在源代码中直接使用 calloc 来获得零化内存。(或者,如果您尝试预先故障它以避免稍后出现页面故障,则该优化将破坏您的尝试。)

如果您永远不打算在写入内存之前读取它,请使用 malloc,这样它可以(可能)为您提供来自其内部空闲列表的脏内存,而不是从操作系统获取新页面。(或者,为小型分配从空闲列表上的内存块中清零一个内存块。)


嵌入式calloc的实现可能会将清零内存的任务交给calloc本身完成,如果没有操作系统,或者不是一款高级的多用户操作系统,则可能会出现这种情况以防止进程之间的信息泄漏。

在嵌入式Linux中,malloc可以使用mmap(MAP_UNINITIALIZED|MAP_ANONYMOUS),但这只适用于某些嵌入式内核,因为在多用户系统中它是不安全的。


263
*alloc 变体很容易记忆 - clear-alloc,memory-alloc,re-alloc。 - Cascabel
52
如果你打算在分配的空间中使用的所有内容都将被设置,则使用malloc()。如果您将保留部分数据未初始化,并且清除未设置的部分会很有益,则使用calloc()。 - Jonathan Leffler
285
calloc不一定更昂贵,因为操作系统可以使用一些技巧来加快速度。我知道FreeBSD会在闲置的CPU时间内运行一个简单的进程,它只是循环清零已释放的内存块,并标记这些进程使用的内存块。因此,当您使用 calloc函数时,它首先尝试找到这些预清零的块之一并将其分配给您-很可能会找到一个这样的块。 - Pavel Minaev
32
如果你的代码因为默认使用零初始化分配变得“更安全”,那么无论是使用malloc还是calloc,你的代码都不够安全。使用malloc是数据需要初始化的良好指标 - 我仅在0字节实际上具有意义的情况下使用calloc。此外,请注意,对于非char类型,calloc未必会做你想要的事情。虽然现在没有人真正使用trap表示或非IEEE浮点数,但这并不能成为认为你的代码真正可移植的借口。 - Steve Jessop
23
“Safer”不是正确的词。我认为“确定性”是更好的术语。具有更多确定性的代码而不是依赖于时间和数据序列的故障,将更容易地隔离故障。使用calloc有时是获得这种确定性的简单方法,而不是显式初始化。 - dennis
显示剩余16条评论

395

一个不太为人所知的区别是,在像Linux这样具有乐观内存分配的操作系统中,malloc返回的指针在程序实际访问它之前并没有得到真正的内存支持。

calloc确实会访问内存(它会写入零),因此您可以确信操作系统正在使用实际的RAM(或交换空间)来支持该分配。这也是为什么它比malloc慢的原因(它不仅需要将其清零,而且操作系统还必须找到一个合适的内存区域,可能会使其他进程被交换出去)

关于malloc行为的更多讨论,请参见此SO问题


59
calloc不必写零。如果分配的块主要由操作系统提供的新的零页面组成,它可以保持那些页面不变。当然,这需要 calloc 调整到操作系统上,而不是在 malloc 上实现的一个通用库函数。另外,实现者也可以在对每个字进行清零之前将其与零进行比较。这样做不会节省任何时间,但它可以避免污染新页面。 - R.. GitHub STOP HELPING ICE
3
有趣的观点。但实际上,这种实现在现实中存在吗? - Isak Savo
12
所有类似于dlmalloc的实现,如果通过mmap映射新的匿名页面(或其等效方式)获取了内存块,则会跳过memset操作。通常这种分配用于较大的内存块,从256k开始。除了我的实现之外,我不知道有任何实现在写零之前对零进行比较。 - R.. GitHub STOP HELPING ICE
1
omalloc也跳过了memset; calloc不需要触及应用程序尚未使用的任何页面(页面缓存)。然而,非常原始的calloc实现会有所不同。 - mirabilos
10
glibc的calloc函数会检查是否从操作系统中获得新内存。如果是这样,它就知道不需要对其进行写入,因为使用mmap(..., MAP_ANONYMOUS)返回的内存已经被清零。 - Peter Cordes
显示剩余2条评论

127
的一个经常被忽视的优点是(符合规范的实现)它可以帮助保护您免受整数溢出漏洞的影响。比较一下:

size_t count = get_int32(file);
struct foo *bar = malloc(count * sizeof *bar);

对抗。
size_t count = get_int32(file);
struct foo *bar = calloc(count, sizeof *bar);

前者可能会导致很小的分配和随后的缓冲区溢出,如果count大于SIZE_MAX/sizeof *bar。后者将自动在这种情况下失败,因为无法创建如此大的对象。
当然,你可能需要留意非符合规范的实现,它们简单地忽略了溢出的可能性……如果这是你所针对的平台的一个问题,你仍然需要手动测试是否有溢出的情况。

23
显然,算术溢出是导致2002年OpenSSH漏洞的原因。OpenBSD关于与内存相关函数存在潜在危险的好文章:http://undeadly.org/cgi?action=article&sid=20060330071917 - Philip P.
5
很有趣。遗憾的是,您链接的文章在开头就存在错误信息。与char相关的例子并不是溢出,而是将结果重新分配回一个char对象时的实现定义转换。 - R.. GitHub STOP HELPING ICE
你应该注意到在那个回复中,你假设了一个32位的size_t,因为当size_t宽度为64位时,我并没有看到任何问题。 - Patrick Schlüter
1
@tristopia:重点不在于代码是否可以在所有实现中被利用,而在于它没有附加假设是不正确/可移植的使用。 - R.. GitHub STOP HELPING ICE
3
如果你的思维方式是“size_t是64位的,所以没有问题”,那就是一种有缺陷的思维方式,这将导致安全漏洞。size_t是一个抽象类型,表示大小,没有理由认为32位数字和size_t(注意:在64位C实现中,“sizeof *bar”的值原则上可能大于2^32)的任意乘积适合size_t - R.. GitHub STOP HELPING ICE
显示剩余6条评论

43
文档让`calloc`看起来像是`malloc`,只是对内存进行了零初始化;但这不是主要的区别! `calloc`的想法是为内存分配抽象出写时复制的语义。当使用`calloc`分配内存时,它们全部映射到一个相同的物理页面,并被初始化为零。当分配的内存中的任何页面被写入时,会分配一个物理页面。例如,这通常用于创建巨大的哈希表,因为空的哈希部分不会备有额外的内存(页面);它们指向单个已初始化为零的页面,甚至可以在进程之间共享。
对虚拟地址的任何写操作都映射到页面,如果该页面是零页面,则分配另一个物理页面,将零页面复制到该页面,并将控制流返回客户端进程。 这与内存映射文件、虚拟内存等方式工作方式相同... 它使用分页。
以下是关于该主题的一则优化故事:http://blogs.fau.de/hager/2007/05/08/benchmarking-fun-with-calloc-and-zero-pages/

27

在分配的内存块大小上没有区别,calloc 只是用物理零位模式填充内存块。实际上,通常假设使用 calloc 分配的内存块中的对象具有初始值,就好像它们被字面值 0 初始化一样,即整数应该具有值 0,浮点变量应具有值 0.0,指针应具有适当的空指针值等。

然而,严格来说,calloc(以及 memset(..., 0, ...))仅保证正确地初始化了类型为 unsigned char 的对象。其他所有内容都不能保证被正确初始化,并且可能包含所谓的“trap representation”,这会导致行为未定义。换句话说,对于除了 unsigned char 之外的任何类型,前面提到的全零位模式可能表示非法值,即陷阱表示。

后来,在 C99 标准的技术勘误之一中,对所有整数类型的行为进行了定义(这是有道理的)。也就是说,在当前 C 语言中,只能使用 calloc(和 memset(..., 0, ...))初始化整数类型。在一般情况下,使用它来初始化其他内容会导致未定义的行为,从 C 语言的角度来看。

实际上,calloc 能够正常工作,这是我们都知道的 :),但您是否想要使用它(考虑到上述内容)由您决定。我个人更喜欢完全避免使用它,而是使用 malloc 并执行自己的初始化。

最后,另一个重要细节是calloc需要内部计算最终块的大小,通过将元素大小乘以元素数量。在这个过程中,calloc必须注意可能的算术溢出。如果无法正确计算请求的块大小,它将导致分配失败(空指针)。而你的malloc版本没有尝试检测溢出。如果发生溢出,它将分配一些“不可预测”的内存量。


根据“另一个重要细节”段落:似乎memset(p, v, n * sizeof type);会有问题,因为n * sizeof type可能会溢出。我猜我需要使用for(i=0;i<n;i++) p[i]=v;循环来编写健壮的代码。 - chux - Reinstate Monica
如果有一种标准的方式来断言实现必须使用全零比特作为空指针(否则拒绝编译),那将是有帮助的,因为存在使用其他空指针表示的实现,但它们相对较少;如果代码不必在这些实现上运行,那么如果可以使用calloc()或memset()来初始化指针数组,则可以更快地运行。 - supercat
@chux 不,如果存在一个具有n个元素的数组,其中一个元素具有大小sizeof type,那么n*sizeof type不会溢出,因为任何对象的最大大小必须小于SIZE_MAX - 12431234123412341234123
@12431234123412341234123 对于大小小于SIZE_MAX的数组是正确的,但这里没有数组。从calloc()返回的指针可能指向超过SIZE_MAX的分配内存。许多实现确实将2个参数的乘积限制为calloc()SIZE_MAX,但C规范并未强制执行该限制。 - chux - Reinstate Monica
@AnT,"算术溢出"和"堆溢出"是同一回事吗? - Sandrious

22

这篇文章来自于Georg Hager's Blog,名为Benchmarking fun with calloc() and zero pages

使用calloc()分配内存时,请求的内存量不会立即分配。相反,所有属于内存块的页面都通过一些MMU魔法连接到一个包含所有零的单个页面上(下面有链接)。如果仅读取这些页面(在原始版本的基准测试中是数组b、c和d),则数据从单个零页面提供,当然适合缓存。至此,关于内存限制的循环内核就讲完了。如果写入页面(无论如何)会导致故障,将映射“真实”页面并将零页面复制到内存。这被称为写时复制,这是一种众所周知的优化方法(我甚至在我的C++讲座中多次教授)。之后,对于该页面,零读取技巧将不再起作用,这就是为什么在插入 - 可能是冗余的 - 初始化循环后性能要低得多的原因。


19

块数:
malloc() 分配单个请求的内存块,
calloc() 分配多个请求的内存块。

初始化:
malloc() - 不清除和初始化分配的内存。
calloc() - 将分配的内存初始化为零。

速度:
malloc() 很快。
calloc() 比 malloc() 慢。

参数和语法:
malloc() 接受一个参数:

  1. 字节数

    • 要分配的字节数

calloc() 接受两个参数:

  1. 长度

    • 要分配的内存块数
  2. 字节数

    • 要在每个内存块中分配的字节数
void *malloc(size_t bytes);         
void *calloc(size_t length, size_t bytes);      

内存分配方式:
malloc 函数从可用的堆中分配所需“size”大小的内存。
calloc 函数分配大小为“num * size”的内存。

名称含义:
malloc 的名字意味着“内存分配”。
calloc 的名字意味着“连续分配”。


1
可爱的回答。简洁而有用。 - mohammadsdtmnd

14

calloc通常相当于malloc+memset设置为0。

明确使用malloc+memset通常稍微更好,尤其是在做以下操作时:

ptr=malloc(sizeof(Item));
memset(ptr, 0, sizeof(Item));

这样更好,因为编译器在编译时知道sizeof(Item)的大小,并且编译器通常会用最优代码将其替换为清零内存的最佳指令。另一方面,如果memsetcalloc中发生,则分配的大小参数不会在calloc代码中编译,并且通常会调用真正的memset,其中包含逐字节填充到长边界,然后循环以sizeof(long)块填充内存,最后逐字节填充剩余空间的代码。即使分配器足够聪明地调用某些aligned_memset,它仍然是一个通用的循环。

有一个值得注意的例外情况,那就是当您需要malloc/calloc一个非常大的内存块(一些2的幂次方千字节)时,此时的分配可能直接从内核中完成。由于操作系统内核通常会清零所有提供的内存以确保安全性,因此聪明的calloc可能直接返回未经过额外清零处理的内存。同样地,如果您只是要分配一些小内存,那么在性能方面使用malloc+memset可能更好。


1
对于提醒我们的+1,即系统库中通用功能的实现不一定比用户代码中的相同操作更快。 - Patrick Schlüter
1
还有第二个原因使得calloc()malloc()慢:大小的乘法。 calloc()需要使用通用乘法(如果size_t是64位,则甚至需要进行非常昂贵的64位* 64位= 64位操作),而malloc()通常具有编译时常量。 - Patrick Schlüter
5
glibc的calloc函数有一些智能功能,可以决定如何最有效地清除返回的内存块,例如有时只需要清除部分区域,而且还有一个展开了的清除方式,最多可以清除9sizeof(size_t)个字节。内存就是内存,以3个字节为单位清除它并不能提高速度,因为你之后可能会用它来保存struct foo { char a, b, c; }。如果你总是要清除整个malloc分配的内存区域,那么calloc总是比malloc+memset更优。此外,calloc还有一个仔细但高效的检查int溢出的方法,用于sizeelements的计算。 - Peter Cordes

10

这里有两个区别。
首先是参数个数,malloc()只需要一个参数(所需内存的字节数),而calloc()需要两个参数。
其次,malloc()不会初始化分配的内存,而calloc()会将分配的内存初始化为0。

  • calloc() 分配一块内存区域,其长度将是其参数的乘积。 calloc用0填充内存并返回第一个字节的指针。如果无法定位足够的空间,则返回一个NULL指针。

语法:ptr_var = calloc(no_of_blocks, size_of_each_block); 即:ptr_var = calloc(n, s);

  • malloc()分配一个请求大小的单块内存,并返回第一个字节的指针。如果它无法找到请求的内存量,则返回一个空指针。

语法:ptr_var = malloc(Size_in_bytes); malloc()函数需要一个参数,该参数是要分配的字节数,而calloc()函数需要两个参数,一个是元素数量,另一个是要为每个元素分配的字节数。此外,calloc()会将分配的空间初始化为零,而malloc()则不会。


9

区别1:

malloc()通常分配内存块并初始化内存段。

calloc()分配内存块并将所有内存块初始化为0。

区别2:

如果您考虑malloc()语法,它只需要1个参数。请考虑下面的示例:

data_type ptr = (cast_type *)malloc( sizeof(data_type)*no_of_blocks );

例如:如果你想要分配 10 个 int 类型的内存块,

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

如果你考虑calloc()的语法,它将需要2个参数。请看下面的示例:
data_type ptr = (cast_type *)calloc(no_of_blocks, (sizeof(data_type)));

例如:如果您想为 int 类型分配 10 个内存块,并将所有内容初始化为零,

int *ptr = (int *) calloc(10, (sizeof(int)));

相似性:

如果没有进行类型转换,malloc()calloc() 默认会返回 void* 类型。


你为什么要将 data_type 和 cast_type 保持不同呢? - Sold Out

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