malloc 只回答一个问题:给我这么多字节。<stdlib.h> 中的两个搭档回答接下来的问题:给我这么多字节,并且清零(calloc),以及我手上的这块内存需要更大(realloc)。
calloc:个数、大小和零
void *calloc(size_t count, size_t size);
它接收两个参数而不是一个,返回的内存全部填充为零字节。
这是最典型的场景:直方图或计数数组,每个位置都必须从零开始。用 malloc 的话你需要写一个循环;calloc 在分配时就完成了清零,而且往往不需要额外开销,因为操作系统分配的新内存页本来就是清零的。
防溢出的理由
拆成两个参数并不是为了好看。假设元素个数是从文件中读出来的:
size_t n = huge_value_from_input;
int *a = malloc(n * sizeof *a); /* the product can wrap around */
int *b = calloc(n, sizeof *b); /* required to detect the overflow */
如果 n * sizeof *a 超出了 size_t 能表示的范围,它会回绕成一个很小的数,malloc 成功分配一个很小的内存块,之后的每次写入都会远远越过末尾。而标准要求 calloc 在这种情况下失败并返回 NULL。当大小来自程序外部时,这个检查很有价值。
“清零”到底是什么意思
calloc 写入的是零字节。对整数和字符来说,这就是值零,正是你想要的。对指针和浮点数来说,在所有主流平台上,全零位分别表示空指针和 0.0,但 C 标准并没有保证这一点。必须严格可移植的代码会显式地给它们赋值。
calloc 与 malloc + memset
int *a = calloc(n, sizeof *a);
int *b = malloc(n * sizeof *b);
if (b != NULL) {
memset(b, 0, n * sizeof *b);
}
两者结果相同。优先用 calloc:只有一行,带溢出检查,而且对于大内存块,它可能根本不需要触碰那块内存。如果你马上就要覆盖每一个字节,就用普通的 malloc:把一兆字节清零后又立刻填满,纯属浪费。
realloc:改变大小
void *realloc(void *p, size_t newsize);
realloc 返回一个 newsize 字节的内存块,其内容与旧内存块相同,长度取两个大小中较小的那个。它可能在原地扩展内存块,也可能分配一块新的,复制内容,再释放旧的。你无法知道是哪一种,所以之后唯一能用的指针就是它返回的那个。
tmp 指针写法
围绕 tmp 的那三行,正是这个函数名声在外的全部原因。下面这种诱人的简写是错的:
/* BUG: leaks the original block when realloc fails */
p = realloc(p, newsize);
if (p == NULL) {
return;
}
realloc 失败时返回 NULL,并且原来的内存块仍然保持已分配、内容不变。把结果直接赋给 p,会覆盖指向那块内存的唯一指针,于是它再也无法被释放:这就是内存泄漏,而且恰恰发生在内存已经紧张的时候。先赋给 tmp,检查 tmp,再提交:
void *tmp = realloc(p, newsize);
if (tmp == NULL) {
/* p is still valid; handle the failure however suits the caller */
return 0;
}
p = tmp;
还有两个值得了解的行为:realloc(NULL, n) 的效果和 malloc(n) 完全相同,这让扩容函数在第一次调用时不需要特殊处理。另外,扩大后新增的字节是未初始化的,只有旧内容会被保留。
扩容数组:标准模式
几乎每个 C 语言容器都使用这种模式:一个长度、一个容量,二者相等时容量翻倍。
这里的关键在于两个设计要点。
翻倍,而不是加一。 每次只增加固定数量,会让每次添加都复制整个数组,填满 n 个元素大约要复制 n² 次。翻倍让复制变得足够少,平均下来每次添加只需常数时间。cap == 0 ? 4 : cap * 2 这个起点让第一次添加不需要单独的分支,因为 realloc(NULL, n) 就是 malloc。
结构体只拥有一次分配。 v.data 由拥有该结构体的一方恰好释放一次。把这条所有权规则写下来(写在注释里,或者让每个 vec_init 都配一个 vec_free),才能让一个不断扩容的容器在函数之间传来传去时不发生泄漏。
缩小
用更小的大小调用 realloc 可以裁剪内存块。当你把数量未知的数据读进一个过大的缓冲区之后,这很有用:
缩小失败并不值得为此中止程序:旧的、更大的内存块仍然有效,内容也仍然正确。这是唯一一种忽略失败才是正确做法的情况;但请注意,它仍然要经过 tmp,因为“不要覆盖正在使用的指针”这条规则不会变。
该用哪一个
| 你需要 | 使用 |
|---|---|
| 一些字节,内容无所谓 | malloc(n * sizeof *p) |
| 若干个元素,并且清零 | calloc(n, sizeof *p) |
| 元素个数来自不可信的输入 | calloc,因为它有溢出检查 |
| 把现有的内存块变大或变小 | 通过 tmp 指针使用 realloc |
| 扩容循环中的第一次分配 | realloc(NULL, n),无需特殊处理 |
这四种分配都用同一个 free 释放,而且只释放一次。
常见错误
p = realloc(p, n):失败时会泄漏。始终通过tmp。- 保留指向内存块内部的旧指针。
realloc之后数据可能已经搬走,所以保存的每个内部指针或由下标算出的地址都失效了。下标在搬移后依然有效,指针则不然。 - 以为新增的字节是零。 只有
calloc会清零。realloc扩大的部分是未初始化的。 calloc(n * size, 1):这样做丢掉了双参数形式存在的意义,也就是溢出检查。realloc成功后又释放旧指针。realloc已经释放过它了,再free一次就是重复释放(double free)。
常见问题
C 语言中 calloc 和 malloc 有什么区别?
有两点区别。calloc(n, size) 分别接收元素个数和元素大小,并检查二者相乘会不会溢出,而 malloc(n * size) 可能悄悄溢出回绕。另外,calloc 会把内存块全部填零,malloc 则保留内存里原来的内容。
C 语言中 realloc 是怎么工作的?
realloc(p, newsize) 返回一个新大小的内存块,并保留原有内容,保留的长度取新旧两个大小中较小的那个。它可能在原地扩大内存块,也可能把它搬走,所以只有返回的指针可以继续使用,旧指针可能已经被释放了。
为什么要把 realloc 的结果赋给一个临时指针?
因为调用失败时,p = realloc(p, n) 会用 NULL 覆盖 p,而原来的内存块仍然处于已分配状态,却再也没有指针指向它,这就是内存泄漏。先赋给 tmp,检查它是否为 NULL,确认无误后再写 p = tmp。
什么时候应该用 calloc 而不是 malloc?
当你确实需要清零的内存时(计数数组、字段需要从空值开始的结构体、只会部分填充的缓冲区),或者当大小是元素个数乘以元素大小、而且可能很大时。如果你反正要覆盖每一个字节,用 malloc 可以省掉清零的工作。