4. realloc 的伸缩术
内存分配这事儿,说白了就是跟操作系统讨空间。malloc 是第一次伸手,calloc 是伸手还要清零,而 realloc 呢?它是「调整」—— 你之前要了 100 字节,现在发现不够了,想扩到 200 字节。或者反过来,要多了,想缩回来。
我个人习惯把 realloc 叫作「内存界的伸缩术」。用好了,代码优雅高效;用不好,内存泄漏、野指针、段错误,一个都跑不掉。
4.1 函数原型:realloc 长什么样?
#include <stdlib.h>
void *realloc(void *ptr, size_t new_size);
参数就两个:
ptr—— 之前用malloc、calloc或realloc返回的指针。如果传NULL,效果等同于malloc(new_size)。new_size—— 新的大小,单位字节。
返回值:成功返回新内存的起始地址,失败返回 NULL,原来的内存块保持不变。
关键点:返回的地址可能和原来一样,也可能完全不一样。别假设指针不变。
4.2 调整内存大小:realloc 到底怎么「伸缩」?
我画了一张图,帮你理解 realloc 的内部逻辑:
你看,realloc 其实是个「聪明」的函数。它先看看能不能原地扩展 —— 如果当前内存块后面还有空闲空间,就直接往后长,指针不变。如果后面被人占了,那就另找一块更大的地方,把旧数据搬过去,再释放旧块。
小技巧:如果你频繁 realloc 一个缓冲区,可以考虑每次翻倍增长(比如 16→32→64),而不是每次只加 1 字节。这样可以减少内存搬移的次数。我在项目里见过有人逐字节 realloc,性能惨不忍睹。
4.3 注意事项与陷阱
嗯,这里要重点说。realloc 的坑,我踩过不止一次。
陷阱一:直接用返回值覆盖原指针
// ❌ 错误写法
int *p = (int *)malloc(10 * sizeof(int));
p = (int *)realloc(p, 20 * sizeof(int)); // 如果 realloc 失败,p 变成 NULL,原内存泄漏!
我曾经在线上代码里犯过这个错。realloc 返回 NULL 时,原来的内存块其实还在。但你直接把 NULL 赋给了 p,原内存的地址就丢了 —— 内存泄漏。
// ✅ 正确写法
int *p = (int *)malloc(10 * sizeof(int));
int *new_p = (int *)realloc(p, 20 * sizeof(int));
if (new_p == NULL) {
// 处理失败,p 仍然有效
free(p);
return -1;
}
p = new_p; // 确认成功后再赋值
陷阱二:new_size 为 0 的行为
标准规定,如果 new_size 为 0,realloc 的行为是实现定义的。在大多数实现中,它相当于 free(ptr) 并返回 NULL。但有些实现可能返回一个不可解引用的非 NULL 指针。
警告:不要依赖 realloc(ptr, 0) 来释放内存。想释放就用 free(),别玩花活。
陷阱三:realloc 后的指针失效
realloc 之后,原来的指针就「悬空」了。如果你有其他变量保存了旧地址,它们就成了野指针。
int *p = malloc(10 * sizeof(int));
int *alias = p; // alias 也指向同一块内存
p = realloc(p, 20 * sizeof(int)); // p 可能变了
// alias 现在可能是野指针!
// 不要再用 alias 访问数据
陷阱四:缩小内存时的数据截断
realloc 缩小内存时,超出的部分会被丢弃。如果你缩小后还想用旧数据,那就等着出 bug 吧。
int *p = malloc(10 * sizeof(int));
// 填充 p[0]~p[9]
p = realloc(p, 5 * sizeof(int)); // p[5]~p[9] 的数据已经没了
// 此时访问 p[6] 是未定义行为
4.4 实战经验:realloc 在动态数组中的应用
我习惯用 realloc 实现一个简单的动态数组。核心思路就是:容量不够时翻倍增长。
#include <stdio.h>
#include <stdlib.h>
typedef struct {
int *data;
size_t size;
size_t capacity;
} DynArray;
void da_init(DynArray *arr) {
arr->data = NULL;
arr->size = 0;
arr->capacity = 0;
}
int da_push(DynArray *arr, int value) {
if (arr->size >= arr->capacity) {
size_t new_cap = arr->capacity == 0 ? 4 : arr->capacity * 2;
int *new_data = (int *)realloc(arr->data, new_cap * sizeof(int));
if (new_data == NULL) {
return -1; // 扩容失败
}
arr->data = new_data;
arr->capacity = new_cap;
}
arr->data[arr->size++] = value;
return 0;
}
void da_free(DynArray *arr) {
free(arr->data);
arr->data = NULL;
arr->size = 0;
arr->capacity = 0;
}
你看,这里每次扩容都用了临时指针 new_data,就是为了避免陷阱一。这个模式我用了很多年,从来没出过问题。
4.5 总结一下 realloc 的核心要点
| 场景 | 行为 | 注意事项 |
|---|---|---|
| 扩大内存,后面有空间 | 原地扩展,返回原指针 | 数据自动保留 |
| 扩大内存,后面没空间 | 新分配 + 拷贝 + 释放旧块 | 原指针失效,用临时变量接收返回值 |
| 缩小内存 | 原地截断或搬移 | 超出新大小的数据丢失 |
| ptr 为 NULL | 等价于 malloc(new_size) | 可以简化代码,但可读性略差 |
| new_size 为 0 | 实现定义,通常等价于 free | 别这么用,老老实实 free() |
realloc 是个好工具,但用的时候多留个心眼。记住三点:用临时变量接返回值、别假设指针不变、缩小内存时数据会丢。做到这三点,realloc 就是你手中的一把利器。
公众号:蓝海资料掘金营,微信deep3321