44、常见错误与陷阱:野指针、数组越界、内存泄漏、缓冲区溢出、悬空指针
说实话,C语言里这些错误,我几乎都踩过。而且不止一次。
你想想看,C给你最大的自由,也给你最大的“坑”。指针随便指、数组随便越、内存申请了不释放……编译器不会拦你,运行时也不会立刻报错。等到程序崩了,你才一脸懵。
这一章,我把最常见的五个“杀手”拎出来,一个一个拆解。每个我都用项目里的真实教训说话。
核心观点:这五个错误,本质上是“内存管理失控”的不同表现形式。理解了内存布局,就理解了它们的根源。
5.1 野指针:指向了“不存在”的地方
野指针,就是指针变量里存了一个“无效地址”。你用它读写,后果不可预测。
为什么会这样?常见三种情况:
- 指针未初始化:局部指针变量默认值是随机的,直接用它就是野指针。
- 指针指向已释放的内存:free()之后没置NULL,后面又用了。
- 指针指向栈上已销毁的变量:函数返回局部变量的地址,外面再用就出事了。
// 野指针示例:返回局部变量地址
int* getValue() {
int a = 100;
return &a; // a是局部变量,函数结束就销毁了
}
int main() {
int* p = getValue();
printf("%d\n", *p); // 野指针!读到的值不确定
return 0;
}
我的习惯:每次声明指针,立刻初始化为NULL。free()之后也马上置NULL。这样就算误用,也能在崩溃前发现。
5.2 数组越界:C语言不帮你检查边界
数组越界,说白了就是访问了“不属于你的内存”。C语言不会像Java那样抛异常,它直接读写相邻内存。
我在项目中遇到过:一个同事写了个循环,下标从0到n,结果数组大小只有n。多写了一个元素,把相邻的另一个变量的值给覆盖了。查了两天才定位到。
// 数组越界示例
int arr[5] = {1,2,3,4,5};
for (int i = 0; i <= 5; i++) { // 注意:i <= 5,越界了
arr[i] = i * 10; // arr[5]写到了数组外面
}
警告:数组越界不一定会立刻崩溃。它可能悄悄破坏数据,等到程序跑远了才出问题。这种bug最难查。
5.3 内存泄漏:申请了不释放,系统慢慢“撑死”
每次malloc()、calloc()、realloc(),都要对应一个free()。只申请不释放,就是内存泄漏。
你想想看,一个长期运行的程序,比如服务器、嵌入式设备,如果每处理一个请求就泄漏一点内存,最终系统会耗尽资源,然后挂掉。
// 内存泄漏示例
void leak() {
int* p = (int*)malloc(100 * sizeof(int));
// 用完了,忘记free()
// p变成野指针,内存泄漏
}
int main() {
for (int i = 0; i < 1000; i++) {
leak(); // 每次泄漏400字节(假设int占4字节)
}
// 程序退出时,内存才被OS回收
return 0;
}
我曾经:写一个网络服务程序,跑了两天突然挂了。用valgrind一查,发现一个循环里每次malloc都没free,泄漏了上百MB。从那以后,我写代码时先写free(),再写malloc()——这叫“先想好怎么还,再借钱”。
5.4 缓冲区溢出:比数组越界更危险
缓冲区溢出,通常发生在字符串操作时。比如strcpy()、sprintf()、gets()这些函数,不检查目标缓冲区大小。
为什么危险?因为它不仅能破坏数据,还能被黑客利用来执行恶意代码。历史上很多安全漏洞都是缓冲区溢出导致的。
// 缓冲区溢出示例
char buffer[10];
strcpy(buffer, "This is a very long string that will overflow!");
// buffer只有10字节,源字符串远超10字节
// 溢出的内容会覆盖栈上其他数据
安全提示:永远不要用gets()、strcpy()、sprintf()这些不安全的函数。改用fgets()、strncpy()、snprintf(),并明确指定缓冲区大小。
5.5 悬空指针:指针指向了“已释放”的内存
悬空指针和野指针有点像,但更具体:它原本指向一块有效的堆内存,但那块内存被free()了,指针没置NULL。后面再用这个指针,就是悬空指针。
嗯,这里要注意:free()只是释放了内存,但指针变量里存的地址值还在。那块内存可能已经被分配给别的变量了。你再去读写,就是在破坏别人的数据。
// 悬空指针示例
int* p = (int*)malloc(sizeof(int));
*p = 42;
free(p); // 释放内存
// p没有置NULL,变成悬空指针
*p = 100; // 危险!写入了已释放的内存
最佳实践:free()之后立即将指针置为NULL。这样后续对*p的操作会直接崩溃,而不是静默破坏数据。崩溃比隐蔽bug好查一百倍。
知识体系总览
下面这张图,把这五个错误的关系和根源梳理清楚了。你看一眼,就能记住它们的本质。
总结:记住这五条,少踩一半坑
| 错误类型 | 本质 | 典型场景 | 预防口诀 |
|---|---|---|---|
| 野指针 | 指针指向无效地址 | 未初始化、返回局部变量地址 | 声明即初始化 |
| 数组越界 | 访问超出数组范围 | 循环条件写错、下标计算错误 | 检查边界,用sizeof |
| 内存泄漏 | 堆内存未释放 | 忘记free、异常路径未释放 | 先写free,再写malloc |
| 缓冲区溢出 | 字符串操作超出缓冲区 | strcpy、sprintf、gets | 用安全函数,指定大小 |
| 悬空指针 | 使用已释放的内存 | free后未置NULL | free后立即置NULL |
我的建议:写C代码时,脑子里时刻绷着一根弦——“这块内存是谁的?什么时候释放?边界在哪?” 养成这个习惯,这五个错误至少能避开80%。
另外,多用工具。valgrind、AddressSanitizer这些,能帮你自动检测内存问题。别等到程序崩了才用,开发过程中就开着。
公众号:蓝海资料掘金营,微信deep3321