8. 动态内存分配:malloc、calloc、realloc、free 函数详解、内存分配失败的处理
动态内存分配,说白了就是程序在运行的时候,跟操作系统“要”一块内存来用。这跟咱们平时定义变量不一样——变量的大小是编译时就定死的,而动态分配的内存,大小可以在运行时决定。我刚开始学C语言那会儿,觉得这玩意儿可有可无,直到有一次做嵌入式项目,需要处理一个大小不确定的数据包……嗯,从那以后,我再也不敢小看动态内存了。
8.1 为什么需要动态内存分配?
你想想看,写程序的时候,有些数据的大小是未知的。比如你要从文件里读一串字符串,长度多少?不知道。或者你要创建一个链表,节点个数?也不确定。这时候,静态数组就不好使了。
静态数组有个毛病:你开大了,浪费内存;开小了,程序可能崩溃。我见过不少新手,上来就定义一个 char buf[1024],觉得1024够用了。结果用户输入了1025个字符……嗯,缓冲区溢出,程序直接挂了。
动态内存分配,就是解决这个问题的。它让你在程序运行时,按需申请内存。用完了,再还回去。说白了,就是“按需分配,用完即还”。
8.2 malloc:最基础的内存分配器
malloc 是动态内存分配的核心函数。它的原型长这样:
#include <stdlib.h>
void *malloc(size_t size);
它接收一个参数:你要申请的字节数。返回一个 void* 指针,指向分配到的内存起始地址。如果分配失败,返回 NULL。
我个人习惯,每次用 malloc 都会检查返回值。为什么?因为嵌入式设备内存有限,分配失败是常有的事。我曾经在一个项目中,因为忘了检查 malloc 返回值,导致程序在内存不足时直接访问了 NULL 指针……结果就是系统复位,用户数据丢失。那教训,够我记一辈子。
核心要点:malloc 分配的内存是未初始化的。里面存的是随机值,别指望它是0。
看个例子:
#include <stdio.h>
#include <stdlib.h>
int main() {
int *p = (int*)malloc(10 * sizeof(int));
if (p == NULL) {
fprintf(stderr, "内存分配失败!\n");
return -1;
}
// 使用内存
for (int i = 0; i < 10; i++) {
p[i] = i * 2;
}
// 打印
for (int i = 0; i < 10; i++) {
printf("%d ", p[i]);
}
free(p);
return 0;
}
这里有个细节:malloc 返回的是 void*,所以需要强制类型转换。在C语言里,其实可以不转,但C++不行。我个人建议转一下,代码更清晰。
8.3 calloc:带初始化的分配
calloc 跟 malloc 很像,但有两个区别:
- 它接收两个参数:元素个数和每个元素的大小
- 它会自动把分配的内存清零
原型:
void *calloc(size_t nmemb, size_t size);
说白了,calloc(n, size) 等价于 malloc(n * size) 然后 memset(..., 0, n * size)。但 calloc 可能更高效,因为操作系统可以直接给你返回已经清零的物理页。
我在项目中遇到过一个问题:用 malloc 分配了一个结构体数组,结果忘了初始化,里面的指针成员全是野指针。后来改成 calloc,所有字段自动变成0,指针变成 NULL,至少不会直接崩溃。嗯,省心不少。
小技巧:如果你需要分配的内存要清零,直接用 calloc。别先 malloc 再 memset,多此一举。
8.4 realloc:调整已分配内存的大小
有时候,你一开始分配的内存不够用了,想扩大一点。或者分配多了,想缩小一点。这时候 realloc 就派上用场了。
void *realloc(void *ptr, size_t new_size);
它接收两个参数:原来的指针和新的字节数。返回调整后的内存地址。注意,返回的地址可能跟原来不一样——因为系统可能需要在别处找一块更大的连续空间。
使用 realloc 有个坑:如果它返回 NULL,原来的内存块不会被释放。所以你不能直接这么写:
// 错误写法
p = realloc(p, new_size); // 如果失败,p 变成 NULL,原来的内存就泄漏了
正确的做法是:
// 正确写法
int *new_p = (int*)realloc(p, new_size);
if (new_p == NULL) {
// 处理失败,但 p 仍然有效
fprintf(stderr, "realloc 失败!\n");
} else {
p = new_p; // 只有成功才更新指针
}
我曾经在代码里犯过这个错误,结果内存泄漏查了好久。后来养成了习惯:永远用一个临时指针接收 realloc 的返回值。
警告:realloc 的第一个参数可以是 NULL。如果 ptr 是 NULL,它的行为就跟 malloc 一样。但我不建议这么用,代码可读性差。
8.5 free:把内存还回去
有借有还,再借不难。动态分配的内存,用完了必须用 free 释放。否则就会造成内存泄漏。
void free(void *ptr);
释放之后,指针就变成了野指针。我建议立即把指针置为 NULL:
free(p);
p = NULL; // 避免野指针
为什么?因为如果你不小心再次 free(p),程序会崩溃。而 free(NULL) 是安全的,什么都不会发生。
还有一点:不要重复释放同一块内存。这叫 double free,是未定义行为。我见过一个同事,在错误处理路径里忘了把指针置 NULL,结果正常路径和错误路径各 free 了一次……嗯,调试了一下午。
8.6 内存分配失败的处理
动态内存分配不是100%成功的。在嵌入式系统里,内存资源尤其紧张。分配失败的原因通常有:
- 内存碎片太多,没有足够大的连续空间
- 系统总内存不足
- 堆空间被耗尽
处理分配失败,有几种常见策略:
| 策略 | 说明 | 适用场景 |
|---|---|---|
| 直接退出 | 打印错误信息,调用 exit() | 关键系统,无法降级运行 |
| 返回错误码 | 让调用者决定如何处理 | 库函数,上层有容错机制 |
| 重试 | 等待一段时间后重新分配 | 临时资源紧张,可能恢复 |
| 使用备用内存池 | 预先分配一块备用内存 | 嵌入式系统,实时性要求高 |
我个人习惯,在嵌入式项目里,会预先分配一个小的“应急内存池”。当 malloc 失败时,从应急池里分配。这样至少能保证系统可以输出错误信息,而不是直接死机。
重要原则:每次调用 malloc/calloc/realloc 后,必须检查返回值是否为 NULL。这不是可选项,是必须项。
8.7 知识体系图
下面这张图,把动态内存分配的核心逻辑串起来了。你看一眼,就能明白整个流程:
8.8 避坑指南
最后,我总结几个常见的坑,都是我曾经踩过的:
- 忘记 free:内存泄漏。程序跑久了,内存被吃光。嵌入式设备尤其明显。
- 重复 free:double free,程序崩溃。解决办法:free 后立即置 NULL。
- 使用已 free 的内存:use-after-free,未定义行为。可能当时没事,但随时会崩。
- 越界访问:分配了10个int,却写了第11个。会破坏堆结构,导致后续分配失败。
- 忘记检查 NULL:直接访问空指针,段错误。这是最常见的错误之一。
嗯,动态内存分配就是这么回事。说白了,就是“借、用、还”三个步骤。但每一步都有细节,稍不注意就会出问题。我建议你写代码的时候,把 malloc 和 free 成对写,就像括号一样。这样不容易漏。
公众号:蓝海资料掘金营,微信deep3321