16、常见内存错误:越界访问、重复释放、释放非堆内存
内存管理这事儿,说难不难,说简单也真不简单。我见过太多人在堆内存上栽跟头了,包括我自己刚入行那会儿。今天咱们就把三个最要命的错误掰开揉碎了讲清楚。
16.1 越界访问:你以为你写的是自己的地盘?
越界访问,说白了就是你申请了10个字节,结果你写了第11个字节。C语言不会拦着你,它觉得你是个成年人,应该为自己的行为负责。但运行时,可能就炸了。
典型场景:数组下标越界、字符串拷贝时忘记算'\0'、memcpy长度算错。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
char *p = (char*)malloc(5); // 只申请了5个字节
if (!p) return -1;
// 危险!这里拷贝了11个字符(含'\0')
strcpy(p, "Hello World"); // 越界访问!
printf("%s\n", p);
free(p);
return 0;
}
这段代码能编译通过,甚至可能正常运行几次。但别高兴太早——它只是还没踩到地雷而已。我在项目中遇到过类似情况,程序在客户现场跑了三天才崩溃,查得我头都大了。
后果:
- 覆盖了相邻内存块的管理信息(free时崩溃)
- 覆盖了其他变量的值(逻辑错误,极难排查)
- 触发段错误(Segmentation Fault)
我的习惯:每次malloc后,我都会在纸上算一遍实际需要的字节数。尤其是字符串,别忘了末尾的'\0'。你想想看,strlen("abc")返回3,但你需要4个字节。
16.2 重复释放:同一个地址,你free了两次
重复释放,也叫double free。这错误看起来很低级,但实际中特别容易发生。尤其是在函数调用链中,你传了个指针出去,结果调用方和接收方都以为该自己负责释放。
#include <stdlib.h>
void bad_free(char *ptr) {
free(ptr); // 第一次释放
}
int main() {
char *p = (char*)malloc(10);
if (!p) return -1;
bad_free(p); // 这里释放了一次
free(p); // 这里又释放了一次 —— 重复释放!
return 0;
}
为什么会这样?因为free之后,指针p的值并没有变成NULL。它仍然指向那块已经被回收的内存。第二次free时,堆管理器就懵了——这地址不在我的管理列表里啊!
我曾经踩过的坑:写一个链表销毁函数时,我在循环里free了节点,又在循环外free了一次头节点。结果头节点已经被当作普通节点释放过了。那次调试花了我整整一个下午。
避坑指南:
- free之后立即将指针置为NULL
- 明确谁申请谁释放的原则
- 使用工具如Valgrind检测
16.3 释放非堆内存:你free了不该free的东西
这个错误更隐蔽。你只能free掉malloc、calloc、realloc返回的地址。栈上的变量、全局变量、字符串常量,这些都不能free。
#include <stdlib.h>
#include <stdio.h>
int global_var = 42;
int main() {
int stack_var = 10; // 栈上变量
char *str = "hello"; // 字符串常量(只读区)
int *heap = (int*)malloc(4); // 堆上变量
free(&stack_var); // 错误!释放栈内存
free(str); // 错误!释放只读区
free(heap); // 正确
return 0;
}
嗯,这里要注意:free(&stack_var)和free(str)都会导致未定义行为。轻则程序崩溃,重则破坏堆管理结构,影响后续所有内存操作。
如何判断一个指针能否被free?
| 指针来源 | 能否free | 说明 |
|---|---|---|
| malloc/calloc/realloc | ✅ 可以 | 标准用法 |
| &局部变量 | ❌ 不可以 | 栈上分配 |
| 字符串常量 | ❌ 不可以 | 只读数据段 |
| 全局变量地址 | ❌ 不可以 | 数据段或BSS段 |
| 函数指针 | ❌ 不可以 | 代码段 |
16.4 知识体系总览
下面这张图把三种常见内存错误的关系和后果梳理清楚了。我建议你多看几遍,形成肌肉记忆。
16.5 我的调试心得
说实话,排查内存错误是C程序员的基本功,也是最磨人的。我个人的经验是:
- 先怀疑自己——90%的内存错误都是自己代码的问题,别一开始就怪编译器或操作系统
- 用工具说话——Valgrind、AddressSanitizer、GDB,这些工具能帮你省下大量时间
- 写防御性代码——free后置NULL、检查malloc返回值、用assert验证指针合法性
一个小技巧:如果你不确定一个指针能不能free,可以在free之前加个打印,看看指针的值。如果它指向的是0x7f开头的地址(栈),或者0x4开头的地址(代码段),那多半不能free。堆地址通常是程序运行时动态分配的,没有固定规律,但你可以通过上下文判断。
好了,这三种错误你记住了吗?越界访问、重复释放、释放非堆内存。每一个我都亲手踩过,每一个都让我印象深刻。希望你读完这一章,能少走一些弯路。
公众号:蓝海资料掘金营,微信deep3321