第二十八章 错误处理与调试:从崩溃到掌控
写C语言代码,说白了就是跟机器打交道。机器很死板,你写错一个分号,它就翻脸不认人。我刚开始学C的时候,最怕的就是程序跑着跑着突然崩了,屏幕上留下一堆看不懂的乱码。后来我才明白,错误处理和调试不是事后补救,而是编程的一部分。
这一章,我们就来聊聊怎么让程序在出错时「优雅地倒下」,以及当它真的崩了时,怎么快速找到元凶。
28.1 errno:系统给你的「暗号」
你有没有遇到过这种情况?调用一个函数,它返回了-1,但你完全不知道哪里出了问题。这时候,errno就是系统留给你的小纸条。
errno是一个全局变量,定义在<errno.h>里。很多系统调用和库函数在出错时,除了返回错误值,还会把具体的错误码塞进errno里。比如:
#include <stdio.h>
#include <errno.h>
#include <string.h>
int main() {
FILE *fp = fopen("不存在的文件.txt", "r");
if (fp == NULL) {
printf("错误码: %d\n", errno);
printf("错误信息: %s\n", strerror(errno));
}
return 0;
}
运行结果会告诉你:错误码是2,对应的信息是「No such file or directory」。嗯,这就很清楚了。
perror() 或 strerror() 打印错误信息。别等到程序崩了再回头查,那时候上下文早就丢了。
28.2 perror:一句话说清楚「为什么」
perror() 是个很贴心的函数。你只需要传一个字符串给它,它就会自动拼接上当前 errno 对应的错误描述,然后打印到标准错误输出(stderr)。
#include <stdio.h>
#include <errno.h>
int main() {
FILE *fp = fopen("不存在的文件.txt", "r");
if (fp == NULL) {
perror("打开文件失败");
}
return 0;
}
输出:打开文件失败: No such file or directory
你看,一句话就把「谁出错了」和「为什么出错」都交代清楚了。我在项目中写日志时,几乎每行都离不开 perror。
perror() 打印的是当前线程的 errno。如果你在出错后又调用了其他可能修改 errno 的函数,那原来的错误码就被覆盖了。所以,检查错误要趁早。
28.3 assert:把「不可能」变成「必须检查」
写代码时,我们经常会有一些「绝对不可能发生」的假设。比如:指针一定不为NULL,数组下标一定不越界。但现实是,这些假设往往就是bug的温床。
assert() 宏就是用来验证这些假设的。它定义在 <assert.h> 里。如果表达式为假,程序会立即终止,并打印出断言失败的位置。
#include <assert.h>
void divide(int a, int b) {
assert(b != 0); // 除数不能为0
printf("%d / %d = %d\n", a, b, a / b);
}
int main() {
divide(10, 2); // 正常
divide(10, 0); // 触发断言
return 0;
}
运行到 divide(10, 0) 时,程序会直接崩掉,并告诉你:Assertion failed: b != 0, file test.c, line 5。
你可能会问:「这不就是让程序崩溃吗?跟没处理有什么区别?」
区别大了。断言是用来捕捉「不应该发生」的错误的。它不是为了处理用户输入错误,而是为了在开发阶段尽早发现程序内部的逻辑漏洞。我个人的经验是:在函数入口处对参数做断言,能省掉一半的调试时间。
assert() 只在 debug 模式下生效,release 模式下会被 #define NDEBUG 禁用。所以别用它来做真正的错误处理。
28.4 GDB调试入门:让程序「停下来」给你看
打印日志虽然好用,但有些bug就像幽灵,你加再多日志也抓不住。这时候,就该请出GDB了。
GDB(GNU Debugger)是Linux下最强大的调试工具。它能让你在程序运行时「暂停」,查看变量、堆栈、内存,甚至修改运行状态。
先看一个最简单的例子:
// bug.c
#include <stdio.h>
int main() {
int arr[5] = {1, 2, 3, 4, 5};
for (int i = 0; i <= 5; i++) { // 越界了!
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
编译时加上 -g 选项:
gcc -g -o bug bug.c
然后启动GDB:
gdb ./bug
在GDB里,你可以这样操作:
| 命令 | 作用 |
|---|---|
break main |
在 main 函数入口设断点 |
run |
运行程序,停在断点处 |
next |
单步执行(不进入函数) |
step |
单步执行(进入函数) |
print i |
打印变量 i 的值 |
backtrace |
查看函数调用栈 |
quit |
退出GDB |
当你用 next 一步步走,看到 i 变成5时,你就知道问题出在哪了——数组下标越界。
next,然后一直按回车,就能快速遍历循环体。
28.5 常见段错误排查:从「一脸懵」到「心中有数」
段错误(Segmentation Fault)是C程序员最熟悉的陌生人。说白了,就是程序试图访问它「不该碰」的内存。
常见的段错误原因有这些:
- 解引用空指针:
int *p = NULL; *p = 10; - 数组越界:访问了
arr[100]而数组只有10个元素 - 栈溢出:递归太深,或者局部变量太大
- 使用已释放的内存:
free()之后还去读写 - 写只读内存:比如修改字符串常量
遇到段错误,别慌。先用GDB跑一遍:
gdb ./你的程序
run
# 等程序崩溃后,GDB会停在出错的位置
backtrace # 看调用栈
print 变量名 # 看变量值
如果不想用GDB,也可以用 dmesg 查看系统日志,或者用 addr2line 把地址翻译成代码行号。
28.6 本章知识体系
下面这张图,帮你把本章的核心内容串起来:
这张图把本章内容分成了三大块:错误处理机制(errno、perror、assert)、调试工具(GDB、段错误排查)、以及常见段错误原因。你可以把它当作一张「调试地图」,遇到问题时,先定位属于哪一块,再对症下药。
好了,这一章的内容就到这里。记住:错误处理和调试不是编程的「售后」,而是编程本身。你花在调试上的每一分钟,都是在为程序的健壮性投资。
公众号:蓝海资料掘金营,微信deep3321