之前我们讲到了过程活动记录(AR),那么如何来操纵AR呢,一个可能的方法是,根据局部变量的地址进行推算,例如对于上面的a函数,执行a函数时的当前AR地址就是参数i的地址偏移8个字节,也就是 ((char*)&i) - 8。
然而,不同的C编译器,以及不同的硬件平台都会产生不同的AR结构布局,甚至在一些平台上,AR根本不会存放到Stack中(也可能放在寄存器里,这样运行速度更快一点)。所以这种方式操纵AR是不通用的。
为此,C语言通过库函数的方式提供了操纵AR的统一方法,那就是setjmp和longjmp函数。
(注意:goto语句不能跳出C语言当前的函数)
1. 作用:
setjmp()和longjmp() 可以实现非局部控制转移即从一个函数到另外一个函数的跳转。
2. 函数原型:
int setjmp(jmp_buf j); void longjmp(jmp_buf j, int i);
setjmp函数设置返回点,保存调用函数的栈环境与j中(相当于保护现场)。 l
ongjmp的作用是使用setjmp保存在j中的栈环境信息返回到setjmp的位置,也就是当执行longjmp时程序又回到setjmp处(相当于恢复现场)。
setjmp有两个作用:
1)保存调用函数的栈环境于j中,返回值为0
2)作为longjmp的返回目标地,返回值为longjmp的第二个参数i,使代码能够知道它是实际上是通过longjmp返回的
当然,当使用longjmp()时,j的内容被销毁。
3. jmp_buf数据类型
typedef struct __jmp_buf_tag jmp_buf[1];
jmp_buf实际是一个数组,内存分配在栈空间中,作为参数传递时是一个指针(指向调用函数的栈帧)。
4. 具体实例
#include <stdio.h> #include <setjmp.h> jmp_buf buf; void haha() { printf("in haha() "); longjmp(buf,1); printf("kaonima "); } int tim=0; int main() { if(setjmp(buf)) { printf("back in main "); tim++; } else { printf("first time through "); haha(); } if(tim<3) longjmp(buf,1); return 0; }
运行结果是:
first time through in haha() back in main back in main back in main
6.异常处理
这里举一个使用这两个函数进行异常处理的例子
#include <stdio.h> #include <stdlib.h> #include <setjmp.h> jmp_buf jb; void f1() { printf("进入f1() "); if(0/*正确执行*/){ } else { longjmp(jb,1); } printf("退出f1() "); } void f2() { printf("进入f2() "); if(1/*正确执行*/) { } else { longjmp(jb, 2); } printf("退出f2() "); } int main() { int r = setjmp(jb); if(r==0){ f1(); f2(); }else if(r==1){ printf("处理错误1 "); exit(1); }else if(r==2){ printf("处理错误2 "); exit(2); } return 0; }
当然完整的异常处理远比这里的代码要复杂,需要考虑异常的嵌套等,这里仅仅给出最简单的思路。
注:不要在C++中使用setjmp和longjmp
C++为异常处理提供了直接支持。除非极特殊需要,不要再重新实现自己的异常机制,尤其需要说明的是,简单的调用setjmp/longjmp有可能带来问题。
#include <stdio.h> #include <stdlib.h> #include <setjmp.h> class MyClass { public: MyClass(){ printf("MyClass::MyClass() ");} ~MyClass(){ printf("MyClass::~MyClass() ");} }; jmp_buf jb; void f1() { MyClass obj; printf("进入f1() "); if(0/*正确执行*/){ } else { longjmp(jb,1); } printf("退出f1() "); } void f2() { printf("进入f2() "); if(1/*正确执行*/) { } else { longjmp(jb, 2); } printf("退出f2() "); } int main() { int r = setjmp(jb); if(r==0){ f1(); f2(); }else if(r==1){ printf("处理错误1 "); exit(1); }else if(r==2){ printf("处理错误2 "); exit(2); } return 0; }
g++编译,程序输出:
MyClass::MyClass()
进入f1()
处理错误1
vc++编译,程序输出:
MyClass::MyClass() 进入f1() MyClass::~MyClass() 处理错误1
longjmp()跳转前局部对象可能并不会析构(g++),也可能析构(VC++),C++标准对此并无明确要求。这种依赖于具体编译器版本的代码是应该避免的。
而C++本身的throw关键字,却能严格保证局部对象构造和析构的成对调用。
参考:
https://blog.csdn.net/smstong/article/details/50728022
https://blog.csdn.net/c1194758555/article/details/52780068
https://blog.csdn.net/qq_33656136/article/details/52732970
C专家编程 6.8节