setjmp与longjmp:异常处理机制、与内存管理的关系、资源清理问题
说实话,C语言里没有try-catch,这让很多从Java或C++转过来的朋友很不适应。但C语言其实有一套自己的“异常处理”方案——就是setjmp和longjmp。这两个函数,说白了就是“标记-跳转”机制。我最早接触它们是在一个嵌入式实时系统里,当时要处理深层嵌套的错误恢复,用返回值一层层往上抛,代码写得跟千层饼似的。后来用了setjmp/longjmp,清爽多了。
1. setjmp与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 ret = setjmp(env);
if (ret == 0) {
printf("第一次调用setjmp,ret=%d\n", ret);
func();
} else {
printf("longjmp跳回,ret=%d\n", ret);
}
return 0;
}
运行结果:
第一次调用setjmp,ret=0
进入func
longjmp跳回,ret=42
setjmp第一次调用时返回0,相当于“注册”了一个跳转点。longjmp则像是一个“传送门”,直接把你传回setjmp的位置,并且让setjmp返回你指定的值(非0)。
2. 与内存管理的关系——一个容易踩的坑
这里有个关键问题:longjmp跳转时,栈上的局部变量会怎样?
嗯,答案是——不确定。C标准说,如果局部变量是volatile的,它的值在longjmp后是确定的;否则,行为未定义。说白了,编译器可能优化掉你的变量,或者恢复成旧值,或者保持新值——全看编译器心情。
我在项目中遇到过这样一个bug:
#include <setjmp.h>
#include <stdio.h>
#include <stdlib.h>
jmp_buf env;
void risky_func() {
int *p = (int*)malloc(sizeof(int) * 100);
if (!p) {
longjmp(env, 1); // 内存泄漏!
}
// 使用p...
free(p);
}
int main() {
if (setjmp(env) != 0) {
printf("出错了,但p的内存已经泄漏了\n");
return 1;
}
risky_func();
return 0;
}
看到了吗?longjmp跳走了,free(p)那行根本没执行。p指向的内存就永远泄漏了。你想想看,如果这个函数被频繁调用,内存会涨得多快。
3. 资源清理问题——如何优雅地处理
那怎么办?总不能不用longjmp吧。其实有几种成熟的方案。
方案一:使用“清理栈”模式
我自己比较喜欢的一种做法,是维护一个资源链表,在longjmp之前统一清理:
#include <setjmp.h>
#include <stdlib.h>
#include <stdio.h>
jmp_buf env;
// 简单的资源链表
typedef struct Resource {
void *ptr;
struct Resource *next;
} Resource;
Resource *res_head = NULL;
void register_resource(void *ptr) {
Resource *r = (Resource*)malloc(sizeof(Resource));
r->ptr = ptr;
r->next = res_head;
res_head = r;
}
void cleanup_all() {
while (res_head) {
Resource *tmp = res_head;
free(res_head->ptr);
res_head = res_head->next;
free(tmp);
}
}
void safe_func() {
int *p = (int*)malloc(sizeof(int) * 100);
if (!p) {
cleanup_all();
longjmp(env, 1);
}
register_resource(p);
// 正常使用...
// 正常退出时清理
cleanup_all();
}
int main() {
if (setjmp(env) != 0) {
printf("异常退出,资源已清理\n");
return 1;
}
safe_func();
return 0;
}
方案二:嵌套setjmp——分层错误处理
有时候我们需要在不同层级做不同的清理。比如底层函数只清理自己的资源,上层函数再处理业务逻辑:
jmp_buf top_env;
jmp_buf mid_env;
void bottom() {
int *data = malloc(1000);
if (!data) {
longjmp(mid_env, 1); // 跳到中层
}
// 使用data...
free(data);
}
void middle() {
if (setjmp(mid_env) != 0) {
// 中层清理自己的资源
printf("中层:清理中间资源\n");
longjmp(top_env, 1); // 继续向上抛
}
bottom();
}
int main() {
if (setjmp(top_env) != 0) {
printf("顶层:最终错误处理\n");
return 1;
}
middle();
return 0;
}
这种分层方式,说白了就是手动实现了try-catch的嵌套结构。每一层负责清理自己的资源,然后决定是否继续向上抛。
4. 实战中的避坑指南
我曾经在一个网络协议栈里吃过longjmp的大亏。当时有个函数在多个地方malloc了内存,然后调用了一个可能longjmp的函数。结果每次异常跳转,内存就泄漏一批。查了两天才找到原因。
总结几条经验:
- 不要在longjmp之后依赖局部变量的值——除非你加了volatile。我习惯把所有可能在longjmp后使用的局部变量都声明为volatile。
- longjmp之前,手动清理所有堆内存——或者用上面说的资源链表方案。别指望编译器帮你做。
- 避免在信号处理函数中使用longjmp——信号处理函数的上下文非常有限,longjmp出去可能导致未定义行为。我见过有人这么干,结果程序随机崩溃。
- longjmp不能跨越不同线程——setjmp和longjmp必须在同一个线程中使用。跨线程跳转?想都别想,行为未定义。
5. 知识体系总览
下面这张图总结了setjmp/longjmp的核心知识点和它们之间的关系:
这张图把setjmp/longjmp的核心机制、内存管理要点、资源清理方案以及常见陷阱都串起来了。你可以把它当作一个快速参考,写代码前瞄一眼,能少踩不少坑。
最后说一句:setjmp/longjmp是C语言里少有的“非局部跳转”机制。它很强大,但也很危险。我个人建议,除非你真的需要跨多层函数跳转,否则还是老老实实用返回值+错误码。毕竟,代码的可维护性比一时的方便重要得多。
公众号:蓝海资料掘金营,微信deep3321