25、动态数组:realloc的使用陷阱与内存碎片
动态数组,说白了就是运行时能自动扩容的数组。C语言里实现它,全靠realloc这个函数。嗯,这函数看着简单,用起来却坑不少。我见过太多人在它上面栽跟头了。
realloc的基本用法
先看个标准用法:
#include <stdlib.h>
int *arr = (int*)malloc(10 * sizeof(int));
if (!arr) exit(1);
// 扩容到20个元素
int *tmp = (int*)realloc(arr, 20 * sizeof(int));
if (!tmp) {
free(arr); // 注意:原内存还在!
exit(1);
}
arr = tmp;
这里有个关键点:永远不要直接用arr = realloc(arr, new_size)。为什么?因为realloc失败时会返回NULL,但原内存块不会被释放。如果你直接覆盖了arr,原指针就丢了,内存泄漏就这么来的。
realloc的三种行为
我个人习惯把realloc的行为分成三种情况:
| 情况 | 行为 | 性能影响 |
|---|---|---|
| 原地扩容 | 当前内存块后面有足够空间,直接扩展 | 极快,无拷贝 |
| 移动扩容 | 后面空间不够,重新分配新块,拷贝旧数据 | O(n)拷贝,较慢 |
| 缩小 | 缩小内存块,通常原地操作 | 快,但可能产生碎片 |
你想想看,如果你频繁扩容,每次都要拷贝大量数据,性能能好吗?我在项目中遇到过一个大数组,每次只加一个元素就用realloc,结果程序跑得比蜗牛还慢。
内存碎片:看不见的杀手
内存碎片是什么?说白了就是内存被切得七零八落,虽然总量够,但找不到一块连续的大空间。
看个典型场景:
// 频繁分配和释放不同大小的块
for (int i = 0; i < 1000; i++) {
char *a = malloc(128);
char *b = malloc(256);
free(a);
char *c = malloc(192); // 可能无法复用a的位置
free(b);
free(c);
}
为什么会这样?因为每次分配的大小不一样,释放后留下的空洞大小不匹配。久而久之,内存就像瑞士奶酪一样,全是小洞。
避坑指南:我踩过的那些坑
坑1:realloc缩小内存后,指针还能用吗?
能。缩小操作通常不会移动数据,只是把多余的空间还给系统。但要注意,缩小的那部分数据就没了。
int *arr = malloc(100 * sizeof(int));
// 填充数据...
arr = realloc(arr, 50 * sizeof(int)); // 后50个元素不可访问
// arr[50] 是未定义行为!
坑2:realloc的第一个参数可以是NULL吗?
可以。当第一个参数是NULL时,realloc的行为等价于malloc。这个特性有时候能简化代码:
int *arr = NULL;
// 第一次分配和后续扩容可以用同一套逻辑
int *tmp = realloc(arr, 100 * sizeof(int));
if (tmp) arr = tmp;
坑3:realloc能代替free吗?
可以。当第二个参数是0时,realloc等价于free。但我不建议这么用,可读性太差了。
如何减少内存碎片?
我曾经在一个嵌入式项目里被内存碎片折磨得够呛。后来总结了几条经验:
- 预分配策略:预估最大容量,一次性分配够。比如动态数组,初始就分配128个元素,不够时翻倍扩容。
- 固定大小分配:尽量用固定大小的内存池,避免大小不一。
- 批量扩容:不要一次只加一个元素,每次扩容翻倍或加固定大小(如64个元素)。
- 及时释放:不再使用的内存尽快还给系统,但注意不要频繁分配释放。
看个翻倍扩容的例子:
typedef struct {
int *data;
size_t size;
size_t capacity;
} DynArray;
bool dynarray_push(DynArray *da, int value) {
if (da->size >= da->capacity) {
size_t new_cap = da->capacity ? da->capacity * 2 : 16;
int *tmp = realloc(da->data, new_cap * sizeof(int));
if (!tmp) return false;
da->data = tmp;
da->capacity = new_cap;
}
da->data[da->size++] = value;
return true;
}
这个模式的好处是:扩容次数少,每次拷贝的数据量可控。平均下来,每个元素的拷贝次数接近常数。
realloc与多线程
嗯,这里要注意。realloc不是线程安全的。多线程环境下,如果你在多个线程里同时操作同一个动态数组,必须加锁。否则,一个线程在realloc移动数据时,另一个线程正在访问旧地址——崩溃就在一瞬间。
总结一下
realloc是个好工具,但用不好就是定时炸弹。记住三点:
- 永远用临时变量接收返回值,检查NULL
- 避免频繁小幅度扩容
- 多线程环境下加锁保护
内存碎片这个问题,说白了就是「分配策略」的问题。你设计得好,碎片就少;设计得随意,系统迟早会还你颜色。我在一个长期运行的服务里见过,因为碎片太多,连续分配4KB都失败,但总内存还剩80%。那排查过程,真是让人头大。
好了,关于realloc和内存碎片,就聊这么多。记住,动态内存管理是门手艺活,多实践、多总结,自然就能避开这些坑。