13、程序跳转与错误处理(setjmp.h & assert.h & errno.h)
错误处理这件事,说实话,很多C语言初学者都不太重视。我早年带团队时,经常看到代码里一堆if判断,但错误信息要么不打印,要么打印得让人看不懂。今天咱们就把C语言里三个最常用的错误处理工具——setjmp.h、assert.h和errno.h——彻底讲明白。
13.1 非局部跳转:setjmp与longjmp
先说说setjmp和longjmp。这俩函数是C语言里比较“野”的存在。它们能让你从一个函数直接跳转到另一个函数,跳过正常的函数调用栈。说白了,就是“时空穿越”。
我曾在嵌入式项目中用过它。当时要处理一个深层嵌套的解析器,遇到致命错误时,如果一层层return回去,代码会变得极其丑陋。用longjmp直接跳回顶层错误处理点,清爽多了。
基本用法
#include <setjmp.h>
#include <stdio.h>
jmp_buf env;
void func() {
printf("进入func\n");
longjmp(env, 42); // 跳回setjmp处,返回值为42
printf("这行不会执行\n");
}
int main() {
int val = setjmp(env);
if (val == 0) {
printf("首次调用setjmp\n");
func();
} else {
printf("从longjmp跳回,返回值: %d\n", val);
}
return 0;
}
运行结果:
首次调用setjmp
进入func
从longjmp跳回,返回值: 42
注意看,func()里longjmp之后的printf根本没执行。程序直接跳回了setjmp的位置,并且setjmp第二次返回了42。
longjmp跳回时,局部变量的值可能不确定。除非你用volatile修饰。- 不要从信号处理函数里调用
longjmp,除非你清楚自己在做什么。 - 跳转后,资源(如打开的文件、malloc的内存)不会自动释放。你得自己清理。
我个人只在两种场景用setjmp/longjmp:一是深层嵌套的错误恢复,二是协程的简单实现。日常业务代码,老老实实用return和错误码就好。
13.2 断言的艺术:assert.h
assert是个好东西。它帮你检查“不可能发生”的情况。如果条件为假,程序会打印错误信息并终止。
#include <assert.h>
#include <stdio.h>
int divide(int a, int b) {
assert(b != 0); // 除数不能为0
return a / b;
}
int main() {
printf("%d\n", divide(10, 2)); // 正常
printf("%d\n", divide(10, 0)); // 触发断言
return 0;
}
运行到第二个divide时,程序会输出类似:
Assertion failed: b != 0, file test.c, line 5
然后直接退出。
什么时候用assert?
- 检查函数前置条件:比如指针不能为NULL,索引不能越界。
- 检查后置条件:比如函数返回值必须在某个范围内。
- 检查不变式:比如循环结束后某个变量应该满足的条件。
发布版本中,assert会被#define NDEBUG禁用。所以不要用assert来做真正的错误处理,它只适合调试阶段。
我曾经在一个网络库中看到有人用assert检查用户输入。结果发布版本一跑,非法输入直接导致程序崩溃——因为assert被禁用了,检查根本没生效。嗯,这是个惨痛的教训。
13.3 errno全局变量
errno是C标准库定义的一个全局变量。很多库函数在出错时会设置它。你想想看,像fopen、malloc、strtol这些函数,失败时都会告诉你具体原因。
#include <stdio.h>
#include <errno.h>
#include <string.h>
int main() {
FILE *fp = fopen("不存在的文件.txt", "r");
if (fp == NULL) {
printf("errno = %d\n", errno);
printf("错误信息: %s\n", strerror(errno));
perror("fopen");
}
return 0;
}
输出:
errno = 2
错误信息: No such file or directory
fopen: No such file or directory
errno的常见值
| errno值 | 宏定义 | 含义 |
|---|---|---|
| 1 | EPERM | 操作不允许 |
| 2 | ENOENT | 文件或目录不存在 |
| 12 | ENOMEM | 内存不足 |
| 13 | EACCES | 权限不足 |
| 22 | EINVAL | 无效参数 |
我曾经在写多线程程序时,发现errno的值莫名其妙。后来才意识到,errno是线程局部存储的。每个线程有自己的errno,不会互相干扰。但如果你在信号处理函数里调用了可能设置errno的函数,记得先保存再恢复。
13.4 perror与strerror
这两个函数是打印错误信息的利器。
- perror:直接打印到
stderr,格式是“你给的字符串: 错误描述”。 - strerror:返回错误码对应的描述字符串,你可以自己决定怎么输出。
#include <stdio.h>
#include <errno.h>
#include <string.h>
int main() {
FILE *fp = fopen("/root/secret.txt", "r");
if (fp == NULL) {
// 方式1:perror
perror("打开文件失败");
// 方式2:strerror + fprintf
fprintf(stderr, "错误码 %d: %s\n", errno, strerror(errno));
}
return 0;
}
输出:
打开文件失败: Permission denied
错误码 13: Permission denied
我个人习惯用perror,因为它简单直接。但如果你需要把错误信息记录到日志文件,或者做国际化处理,strerror更灵活。
13.5 知识体系总览
下面这张图帮你理清这三个头文件的关系和适用场景:
13.6 总结
这三个头文件,各有各的用处:
- setjmp.h:用于非局部跳转,适合深层错误恢复。但用起来要小心,资源管理得自己操心。
- assert.h:调试利器,帮你尽早发现逻辑错误。发布版本记得禁用。
- errno.h:标准错误码机制,配合
perror和strerror使用,让错误信息一目了然。
我在实际项目中,通常这样组合使用:调试阶段用assert检查内部逻辑,运行时用errno处理外部错误(比如文件读写失败),而setjmp/longjmp只在极少数特殊场景才出手。你想想看,大部分时候,简单的if判断加错误码返回,其实就够用了。
公众号:蓝海资料掘金营,微信deep3321