8、指针与动态内存分配:malloc、calloc、realloc、free、内存泄漏、野指针
动态内存分配,说白了就是让程序在运行时“按需”向操作系统申请内存。这跟咱们之前讲的数组不一样——数组的大小是编译时就定死的,而动态分配可以灵活调整。我在做嵌入式项目时,经常遇到传感器数据量不确定的情况,这时候动态分配就是救命稻草。
8.1 为什么需要动态内存分配?
你想想看,如果写一个程序要处理用户输入的数据,你事先不知道用户会输入多少条。用固定数组?要么浪费空间,要么不够用。动态分配就能完美解决这个问题。
我个人习惯把动态内存分配比作“借房子住”——你需要多大就借多大,住完了还回去。而静态分配就像“自己盖房子”,大小从一开始就定死了。
8.2 malloc:最基础的内存申请
malloc 是动态内存分配的入门函数。它的原型很简单:
void *malloc(size_t size);
它返回一个 void* 指针,指向一块大小为 size 字节的内存。注意,它不会初始化这块内存——里面的数据是随机的。
核心要点:malloc 返回的是 void*,需要强制类型转换成你需要的指针类型。
来看个实际例子:
int *p = (int*)malloc(10 * sizeof(int));
if (p == NULL) {
// 处理内存分配失败
printf("内存不够了!\n");
return -1;
}
// 现在 p 指向一块可以存放 10 个 int 的内存
我在项目中遇到过这样的情况:有个同事忘了检查 malloc 的返回值,结果在内存紧张的系统上程序直接崩溃了。嗯,检查 NULL 返回值是个好习惯,千万别偷懒。
8.3 calloc:带初始化的内存申请
calloc 和 malloc 很像,但有两点不同:
- 它会将分配的内存全部初始化为 0
- 它接受两个参数:元素个数和每个元素的大小
void *calloc(size_t nmemb, size_t size);
举个例子:
int *p = (int*)calloc(10, sizeof(int));
// 这 10 个 int 全部被初始化为 0
我个人习惯在需要清零的场景用 calloc,比如初始化一个结构体数组。但要注意,calloc 比 malloc 稍慢一点,因为它多了一步清零操作。
8.4 realloc:调整已分配内存的大小
有时候你申请的内存不够用了,或者太多了想缩小。realloc 就是干这个的:
void *realloc(void *ptr, size_t new_size);
它接受一个之前分配好的指针,以及一个新的大小。它会尽量在原有地址上扩展,如果不行就找一块新地方,然后把旧数据拷贝过去。
避坑指南:我曾经因为 realloc 踩过一个坑——直接用原来的指针接收返回值。如果 realloc 失败返回 NULL,原来的指针就丢了,造成内存泄漏。正确做法是用临时指针:
int *temp = (int*)realloc(p, 20 * sizeof(int));
if (temp != NULL) {
p = temp; // 只有成功才更新指针
} else {
// 处理失败,p 仍然指向原来的内存
printf("扩展失败,原数据还在\n");
}
8.5 free:用完记得还回去
动态申请的内存,用完之后必须用 free 释放:
void free(void *ptr);
释放后,这块内存归还给系统,可以被重新分配。但要注意:
- 只能释放通过 malloc/calloc/realloc 获得的内存
- 不能重复释放同一块内存
- 释放后,原来的指针变成“野指针”
我的习惯:每次 free 之后,立刻把指针置为 NULL。这样即使不小心再次 free,也不会出问题。
free(p);
p = NULL; // 好习惯!
8.6 内存泄漏:看不见的杀手
内存泄漏是指你申请了内存,但忘了释放。程序跑得越久,泄漏的内存越多,最终可能导致系统崩溃。
我曾经在一个长期运行的服务器程序里遇到过:每次处理请求都 malloc 一块内存,但忘了 free。结果跑了三天后,系统内存被吃光,服务挂了。排查起来特别痛苦。
常见的内存泄漏场景:
- 函数内 malloc 了内存,但提前 return 了,没来得及 free
- 结构体嵌套时,只释放了外层,忘了释放内层指针指向的内存
- 循环中不断 malloc,但只在循环外 free 了一次
8.7 野指针:最危险的敌人
野指针是指向“已释放”或“无效”内存的指针。使用野指针会导致不可预料的后果——可能崩溃,可能数据错乱,甚至可能被黑客利用。
野指针的常见来源:
- free 之后没有置 NULL
- 返回了局部变量的地址
- 指针越界访问
我曾经犯过的错:写了一个函数返回局部数组的地址,调用方拿到指针后欢天喜地地使用,结果数据全是乱的。因为局部数组在函数返回时已经被销毁了。
// 错误示范:返回局部变量的地址
int* bad_function() {
int local = 42;
return &local; // 危险!函数返回后 local 就不存在了
}
// 正确做法:用动态分配
int* good_function() {
int *p = (int*)malloc(sizeof(int));
if (p) *p = 42;
return p;
}
8.8 知识体系总览
下面这张图总结了本章的核心内容,帮你理清思路:
8.9 总结与避坑清单
动态内存分配是 C 语言中最强大也最容易出错的地方。记住这几条铁律:
| 操作 | 注意事项 |
|---|---|
| malloc/calloc | 必须检查返回值是否为 NULL |
| realloc | 用临时指针接收返回值,别直接覆盖原指针 |
| free | 释放后立即置 NULL,避免重复释放 |
| 野指针 | 绝不使用已释放的指针,绝不返回局部变量地址 |
| 内存泄漏 | 谁申请谁释放,成对出现 |
我的个人建议:在嵌入式系统中,尽量少用动态分配。如果非用不可,一定要做好统计和监控。我习惯在程序里加一个计数器,记录当前分配了多少内存,方便排查泄漏。
嗯,动态内存分配就讲到这里。记住这些要点,写代码时多留个心眼,你就能避开大部分坑了。