程序员应了解的那些事(16)C语言中利用setjmp和longjmp做异常处理 / 不要在C++中使用setjmp和longjmp
错误处理是任何语言都需要解决的问题,只有不能保证100%的正确运行,就需要有处理错误的机制。异常处理就是其中的一种错误处理方式。
<1>、过程活动记录(Active Record)
C语言中每当有一个函数调用时,就会在堆栈(Stack)上准备一个被称为AR的结构,抛开具体编译器实现细节的不同,这个AR基本结构如下所示。每当遇到一次函数调用的语句,C编译器都会产生出汇编代码来在堆栈上分配这个AR。例如下面的C代码:
每当遇到一次函数调用的语句,C编译器都会产生出汇编代码来在堆栈上分配这个AR。例如下面的C代码:
void a(int i)
{
if(i==0){
i = 1;
}
else
{
printf("i = %d \n", i);
}
}
int main(int argc, char** argv)
{
a(1);
}
当程序运行后执行到printf()语句时,堆栈上的AR布局如下:
<2>通过setjmp和longjmp操纵AR,完成任意跳转
那么如何来操纵AR呢,一个可能的方法是,根据局部变量的地址进行推算,例如对于上面的a函数,执行a函数时的当前AR地址就是参数i的地址偏移8个字节,也就是 ((char*)&i) - 8。然而,不同的C编译器,以及不同的硬件平台都会产生不同的AR结构布局,甚至在一些平台上,AR根本不会存放到Stack中。所以这种方式操纵AR是不通用的。为此,C语言通过库函数的方式提供了操纵AR的统一方法,那就是setjmp和longjmp函数。
int setjmp(jmp_buf jb);
void longjmp(jmp_buf jb, int r);
setjmp用于保存当前AR到jb变量中;
而longjmp用于设置当前AR为jb,并跳转到调用setjmp();之后的第一个语句处。其结果就相当于回到了setjmp()刚执行完毕,只是偷偷的修改了setjmp的返回值。
setjmp()第一次调用时总是返回0,而通过longjmp(jb,r)跳转后其返回值总是被修改为r,并且r不能为0。这样程序中就很容易根据setjmp()的返回值来判断是否是longjmp()导致了跳转才执行到此。
setjmp/longjmp主要从嵌套的函数调用中跳出来。
#include <stdio.h>
#include <setjmp.h>
jmp_buf jb;
void a();
void b();
void c();
int main()
{
if(setjmp(jb)==0){
a();
}
printf("after a(); \n");
return 0;
}
void a()
{
b();
printf("a() is called\n");
}
void b()
{
c();
printf("b() is called\n");
}
void c()
{
printf("c() is called\n");
longjmp(jb, 1);
}
在c()中可以直接跳转到main()中,实际上longjmp不限制跳转的目的地,可以跳转到任意位置并恢复当时的堆栈环境(堆栈平衡)。
<3>C语言中实现异常处理
异常处理是错误处理的一种方式,C语言中更常用的错误处理方式是检测函数返回值。
#include <stdio.h>
int f1()
{
if(1/*正确执行*/) { return 1; }
else { return -1; }
}
int f2()
{
if(0/*正确执行*/) { return 1; }
else { return -1; }
}
int main()
{
if(f1()<0){
printf("错误处理1\n");
exit(1);
}
if(f2()<0){
printf("错误处理2\n");
exit(2);
}
return 0;
}
上面代码显示了常见的C语言错误处理方式。严谨的软件开发中,必须检测每一次函数调用可能出现的错误,并做相应的处理。造成的后果就是冗长繁琐的代码。为了统一处理错误,C++,C#,Java等现代语言引入了异常处理机制。同样功能的C++代码大概如下:
#include <stdio.h>
class Ex1{
};
class Ex2{
};
void f1()
{
printf("进入f1()\n");
if(0/*正确执行*/){ }
else {
throw Ex1();
}
printf("退出f1()\n");
}
void f2()
{
printf("进入f2()\n");
if(1/*正确执行*/) { }
else {
throw Ex2();
}
printf("退出f2()\n");
}
int main()
{
try{
f1();
f2();
}catch(Ex1 &ex){
printf("处理错误1\n");
exit(1);
}
catch(Ex2 &ex){
printf("处理错误2\n");
exit(2);
}
return 0;
}
程序输出:
进入f1()
处理错误1
可见,异常处理让代码看起来更加整洁,逻辑代码在一起,错误处理代码在一起。throw后面的语句不再执行,执行流直接跳转到最近的try对应的catch块。可以推测,
throw要负责两件事情:(1)完成跳转;(2)恢复堆栈AR;
try则负责保存当前AR。
可见这与setjmp/longjmp基本相当。于是可以在C中近似写成:
#include <stdio.h>
#include <stdlib.h>
#include <setjmp.h>
jmp_buf jb;
void f1()
{
printf("进入f1()\n");
if(0/*正确执行*/){ }
else {
longjmp(jb,1);
}
printf("退出f1()\n");
}
void f2()
{
printf("进入f2()\n");
if(1/*正确执行*/) { }
else {
longjmp(jb, 2);
}
printf("退出f2()\n");
}
int main()
{
int r = setjmp(jb);
if(r==0){
f1();
f2();
}else if(r==1){
printf("处理错误1\n");
exit(1);
}else if(r==2){
printf("处理错误2\n");
exit(2);
}
return 0;
}
//当然完整的异常处理远比这里的代码要复杂,需要考虑异常的嵌套等,这里仅仅给出最简单的思路。
<4> 不要在C++中使用setjmp和longjmp
C++为异常处理提供了直接支持。除非极特殊需要,不要再重新实现自己的异常机制,尤其需要说明的是,简单的调用setjmp/longjmp有可能带来问题。
longjmp()跳转前局部对象可能并不会析构(g++),也可能析构(VC++),C++标准对此并无明确要求。这种依赖于具体编译器版本的代码是应该避免的。而C++本身的throw关键字,却能严格保证局部对象构造和析构的成对调用。
<示例>
#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(0/*正确执行*/){ }
else {
longjmp(jb,1);
}
printf("退出f1()\n");
}
void f2()
{
printf("进入f2()\n");
if(1/*正确执行*/) { }
else {
longjmp(jb, 2);
}
printf("退出f2()\n");
}
int main()
{
int r = setjmp(jb);
if(r==0){
f1();
f2();
}else if(r==1){
printf("处理错误1\n");
exit(1);
}else if(r==2){
printf("处理错误2\n");
exit(2);
}
return 0;
}
输出结果:
g++编译,程序输出:
MyClass::MyClass()
进入f1()
处理错误1
vc++编译,程序输出:
MyClass::MyClass()
进入f1()
MyClass::~MyClass()
处理错误1
longjmp()跳转前局部对象
<5>辩证看待异常处理
为实现异常处理,C++编译器为此必须做更多的工作,也必然导致在AR中直接或间接地存放更多的信息,并产生操作这些信息的汇编代码,最终必然导致运行效率的降低。
另一方面,已经存在大量没有严格使用异常处理C++函数库和类库,兼容的C库更是没有异常的概念,历史的包袱让C++很难完全采用异常处理。在这个方面,Java和C#从头开始,重要的库都实现了标准的异常处理规范,完全采用异常机制切实可行。
有趣的是C++11在标准中删除了异常规范,而且添加了 noexcept关键字来声明一个函数不会抛出异常,可见异常并不受欢迎。
然而,C++的STL广泛使用异常,所以实际上使用了STL的C++程序是不可能禁用异常的,要是没有了STL,C++又有什么优势了呢?C++在不断的矛盾冲突中向前发展着。
上一篇: opencv python 轮廓抠图粘贴到另外一张图像上
下一篇: (三)深度学习计算 -- 2