4、内存安全:malloc/calloc/realloc返回值检查、内存泄漏检测、野指针防范、缓冲区溢出防护
内存安全这个话题,说实话,是C语言里最容易翻车的地方。我见过太多项目,功能跑得好好的,上线一周就开始随机崩溃。查到最后,十有八九是内存问题。你想想看,一个指针指错了地方,整个系统都可能跟着遭殃。
今天我们就来聊聊内存安全的四个核心问题。每个问题我都踩过坑,有些坑到现在想起来还心有余悸。
4.1 malloc/calloc/realloc返回值检查
很多人写代码时,malloc完直接就用。我年轻时也这么干过。直到有一次,一个嵌入式设备在内存不足时,malloc返回了NULL,程序直接往NULL地址写数据——嗯,系统当场死机。
从那以后,我养成了一个习惯:每次动态分配内存,必须检查返回值。
核心原则:malloc、calloc、realloc 返回 NULL 时,绝对不能继续使用返回的指针。
来看一个典型的错误写法:
// 错误示范——别学我
int *p = (int *)malloc(100 * sizeof(int));
*p = 42; // 如果malloc失败,这里就崩了
正确的做法是这样:
// 正确做法
int *p = (int *)malloc(100 * sizeof(int));
if (p == NULL) {
// 处理错误,比如返回错误码或记录日志
fprintf(stderr, "malloc failed!\n");
return -1;
}
*p = 42;
这里有个细节我想提一下。calloc 和 realloc 同样需要检查。calloc 会帮你把内存清零,但分配失败时一样返回 NULL。realloc 更特殊——它失败时,原来的内存块还在,但返回 NULL。所以你不能直接 p = realloc(p, new_size),否则失败后原指针就丢了。
我的习惯:realloc 时先用临时变量接返回值,确认成功后再赋值给原指针。
// realloc 的安全写法
int *tmp = (int *)realloc(p, new_size * sizeof(int));
if (tmp == NULL) {
// 处理错误,p 仍然有效
fprintf(stderr, "realloc failed!\n");
return -1;
}
p = tmp; // 确认成功后再赋值
4.2 内存泄漏检测
内存泄漏,说白了就是申请了内存没释放。一次两次没事,但嵌入式设备跑个几天几周,内存慢慢被吃光,系统就挂了。
我曾经维护过一个通信网关项目,设备运行三天后必重启。查了整整一周,最后发现是一个消息队列的处理函数里,每次处理完消息都忘了 free 掉 payload。每次泄漏 256 字节,三天下来累积了几十 MB,系统就扛不住了。
怎么检测内存泄漏?我总结了几种方法:
- 静态代码审查:每次 malloc 都要对应一个 free,成对出现。我习惯在代码里用注释标记
/* malloc */和/* free */,方便对照。 - 工具检测:Linux 下用 Valgrind,嵌入式环境可以用 mtrace 或者自己写内存分配钩子。
- 手动计数:写一个简单的内存分配计数器,每次 malloc 加一,free 减一。程序退出时检查计数是否为 0。
这里给一个轻量级的内存泄漏检测示例,适合嵌入式环境:
// 简易内存泄漏检测
static int alloc_count = 0;
void *my_malloc(size_t size) {
void *p = malloc(size);
if (p != NULL) {
alloc_count++;
}
return p;
}
void my_free(void *p) {
if (p != NULL) {
free(p);
alloc_count--;
}
}
// 程序退出时调用
void check_leak(void) {
if (alloc_count != 0) {
printf("Warning: %d memory blocks not freed!\n", alloc_count);
}
}
注意:这个简易方法只能检测数量是否匹配,不能定位具体泄漏位置。生产环境建议用更专业的工具。
4.3 野指针防范
野指针,就是指向无效内存的指针。它比 NULL 指针更危险——NULL 指针至少会立刻崩溃,野指针可能悄悄破坏数据,等到你发现时已经晚了。
野指针的三大来源:
- 未初始化:局部指针变量没赋初值,里面是随机值。
- 释放后未置空:free 之后指针还在,指向的内存已经归还系统。
- 指向栈变量:函数返回后,栈上的局部变量就不存在了。
我自己的防御策略很简单:
- 指针定义时立即初始化为 NULL。
- free 之后立即把指针置为 NULL。
- 绝不返回局部变量的地址。
// 野指针防范示例
void safe_free(void **p) {
if (p != NULL && *p != NULL) {
free(*p);
*p = NULL; // 置空,防止野指针
}
}
// 使用方式
int *data = (int *)malloc(100 * sizeof(int));
// ... 使用 data ...
safe_free((void **)&data);
// 此时 data == NULL,再使用就会立刻崩溃,方便排查
一个小技巧:我习惯在 free 之后加一行 data = NULL;,虽然多打几个字,但能省去后面几小时的调试时间。
4.4 缓冲区溢出防护
缓冲区溢出,是C语言里最臭名昭著的问题之一。往一个固定大小的数组里写入了超出容量的数据,就会覆盖相邻内存。轻则数据错乱,重则被利用执行恶意代码。
我记得有一次,一个串口通信模块的接收缓冲区只开了 64 字节,但协议里最长的一帧数据有 128 字节。结果呢?数据一多,缓冲区后面的变量全被覆盖了,系统状态机直接跑飞。
防护措施其实不复杂,核心就一句话:永远不要相信输入的长度。
常用的防护手段:
| 方法 | 说明 | 示例 |
|---|---|---|
| 使用带长度限制的函数 | 用 strncpy 代替 strcpy,用 snprintf 代替 sprintf | strncpy(dst, src, dst_size - 1); |
| 手动检查长度 | 写入前先判断数据长度是否超过缓冲区 | if (len > buf_size) { /* 截断或报错 */ } |
| 使用安全封装 | 自己封装带边界检查的读写函数 | safe_memcpy(dst, src, dst_size, src_size); |
来看一个实际例子:
// 缓冲区溢出防护示例
#define BUF_SIZE 64
char buffer[BUF_SIZE];
char *input = "这是一段很长的数据,可能会超过缓冲区大小...";
// 错误做法
strcpy(buffer, input); // 如果 input 超过 63 字节,就溢出了
// 正确做法
strncpy(buffer, input, BUF_SIZE - 1);
buffer[BUF_SIZE - 1] = '\0'; // 确保字符串以 '\0' 结尾
注意:strncpy 不会自动添加 '\0',如果源字符串长度大于等于目标缓冲区大小,目标字符串就不会以 '\0' 结尾。所以一定要手动加。
知识体系总览
下面这张图,是我自己整理的内存安全知识框架。四个问题环环相扣,任何一个环节出问题,都可能让整个系统崩溃。
这四个问题,说白了就是内存管理的四个维度。返回值检查是入口把关,内存泄漏是出口管理,野指针是过程控制,缓冲区溢出是边界防护。任何一个维度出问题,都会导致内存安全漏洞。
我个人建议,在项目初期就把这些检查机制写进代码规范里。不要等到出了问题再补,那时候代价就大了。你想想看,一个内存泄漏查三天,够你写多少行代码了?
总结一句话:内存安全没有捷径,只有养成好习惯,才能少踩坑。