之前我们讲到了过程活动记录(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[];
jmp_buf实际是一个数组,内存分配在栈空间中,作为参数传递时是一个指针(指向调用函数的栈帧)。
4. 具体实例
#include <stdio.h>
#include <setjmp.h>
jmp_buf buf;
void haha()
{
printf("in haha()\n");
longjmp(buf,);
printf("kaonima\n");
}
int tim=; int main()
{
if(setjmp(buf))
{
printf("back in main\n");
tim++;
}
else
{
printf("first time through\n");
haha();
}
if(tim<) longjmp(buf,);
return ;
}
运行结果是:
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()\n");
if(/*正确执行*/){ }
else {
longjmp(jb,);
}
printf("退出f1()\n");
}
void f2()
{
printf("进入f2()\n");
if(/*正确执行*/) { }
else {
longjmp(jb, );
}
printf("退出f2()\n");
} int main()
{
int r = setjmp(jb);
if(r==){
f1();
f2();
}else if(r==){
printf("处理错误1\n");
exit();
}else if(r==){
printf("处理错误2\n");
exit();
}
return ;
}
当然完整的异常处理远比这里的代码要复杂,需要考虑异常的嵌套等,这里仅仅给出最简单的思路。
注:不要在C++中使用setjmp和longjmp
C++为异常处理提供了直接支持。除非极特殊需要,不要再重新实现自己的异常机制,尤其需要说明的是,简单的调用setjmp/longjmp有可能带来问题。
#include <stdio.h>
#include <stdlib.h>
#include <setjmp.h> class MyClass
{
public:
MyClass(){ printf("MyClass::MyClass()\n");}
~MyClass(){ printf("MyClass::~MyClass()\n");}
};
jmp_buf jb; void f1()
{
MyClass obj;
printf("进入f1()\n");
if(/*正确执行*/){ }
else {
longjmp(jb,);
}
printf("退出f1()\n");
}
void f2()
{
printf("进入f2()\n");
if(/*正确执行*/) { }
else {
longjmp(jb, );
}
printf("退出f2()\n");
} int main()
{
int r = setjmp(jb);
if(r==){
f1();
f2();
}else if(r==){
printf("处理错误1\n");
exit();
}else if(r==){
printf("处理错误2\n");
exit();
}
return ;
}
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节