13、程序跳转与错误处理(setjmp.h & assert.h & errno.h)

错误处理这件事,说实话,很多C语言初学者都不太重视。我早年带团队时,经常看到代码里一堆if判断,但错误信息要么不打印,要么打印得让人看不懂。今天咱们就把C语言里三个最常用的错误处理工具——setjmp.hassert.herrno.h——彻底讲明白。

13.1 非局部跳转:setjmp与longjmp

先说说setjmplongjmp。这俩函数是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标准库定义的一个全局变量。很多库函数在出错时会设置它。你想想看,像fopenmallocstrtol这些函数,失败时都会告诉你具体原因。

#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 知识体系总览

下面这张图帮你理清这三个头文件的关系和适用场景:

C语言错误处理三剑客 setjmp.h 非局部跳转 assert.h 调试断言 errno.h 错误码机制 核心函数 setjmp() longjmp() jmp_buf 类型 核心函数 assert() NDEBUG 宏 核心函数 errno 变量 perror() strerror() 适用场景 深层错误恢复 协程实现 适用场景 调试阶段检查 前置/后置条件 适用场景 系统调用错误 库函数错误诊断

13.6 总结

这三个头文件,各有各的用处:

  • setjmp.h:用于非局部跳转,适合深层错误恢复。但用起来要小心,资源管理得自己操心。
  • assert.h:调试利器,帮你尽早发现逻辑错误。发布版本记得禁用。
  • errno.h:标准错误码机制,配合perrorstrerror使用,让错误信息一目了然。

我在实际项目中,通常这样组合使用:调试阶段用assert检查内部逻辑,运行时用errno处理外部错误(比如文件读写失败),而setjmp/longjmp只在极少数特殊场景才出手。你想想看,大部分时候,简单的if判断加错误码返回,其实就够用了。


公众号:蓝海资料掘金营,微信deep3321