天天看点

C语言异常与断言接口与实现

在c语言中,标准库函数setjmp和longjmp形成了结构化异常工具的基础。简单的说就是setjmp实例化处理程序,而longjmp产生异常

setjmp和longjmp是c语言所独有的,它们部分弥补了c语言有限的转移能力。与刺激的abort()和exit()相比,goto语句看

起来是处理异常的更可行方案。不幸的是,goto是本地的:它只能跳到所在函数内部的标号上,而不能将控制权转移到所在程序的任意地点(当然,除非你的所

有代码都在main体中)。

为了解决这个限制,c函数库提供了setjmp()和longjmp()函数,它们分别承担非局部标号和goto作用。头文件<setjmp.h>申明了这些函数及同时所需的jmp_buf数据类型。

函数说明:

<code>int setjmp(jmp_buf env)</code>

建立本地的<code>jmp_buf</code>缓冲区并且初始化,用于将来跳转回此处。这个子程序保存程序的调用环境于<code>env</code>参数所指的缓冲区,<code>env</code>将被<code>longjmp</code>使用。如果是从<code>setjmp</code>直接调用返回,<code>setjmp</code>返回值为0。如果是从<code>longjmp</code>恢复的程序调用环境返回,<code>setjmp</code>返回非零值。

<code>void longjmp(jmp_buf env, int value)</code>

恢复<code>env</code>所指的缓冲区中的程序调用环境上下文,<code>env</code>所指缓冲区的内容是由<code>setjmp</code>子程序调用所保存。<code>value</code>的值从<code>longjmp</code>传递给<code>setjmp</code>。<code>longjmp</code>完成后,程序从对应的<code>setjmp</code>调用处继续执行,如同<code>setjmp</code>调用刚刚完成。如果<code>value</code>传递给<code>longjmp</code>零值,<code>setjmp</code>的返回值为1;否则,<code>setjmp</code>的返回值为<code>value</code>。

成员类型:

<code>jmp_buf</code>

数组类型,例如:<code>struct int[16]</code>或<code>struct __jmp_buf_tag</code>,用于保存恢复调用环境所需的信息。

jmp_buf 的定义:

这个是 setjmp.h 里的一行定义,把一个 struct 定义成一个数组。这样,在声明 jmp_buf 的时候,可以把数据分配到堆栈上。但是作为参数传递的时候则作为一个指针。

原理非常简单:

1.setjmp(j)设置“jump”点,用正确的程序上下文填充jmp_buf对象j。这个上下文包括程序存放位置、栈和框架指针,其它重要的寄存器和内存数据。当初始化完jump的上下文,setjmp()返回0值。

2.

以后调用longjmp(j,r)的效果就是一个非局部的goto或“长跳转”到由j描述的上下文处(也就是到那原来设置j的setjmp()处)。当作

为长跳转的目标而被调用时,setjmp()返回r或1(如果r设为0的话)。(记住,setjmp()不能在这种情况时返回0。)

通过有两类返回值,setjmp()让你知道它正在被怎么使用。当设置j时,setjmp()如你期望地执行;但当作为长跳转的目标时,setjmp()就从外面“唤醒”它的上下文。你可以用longjmp()来终止异常,用setjmp()标记相应的异常处理程序。

一个简单的例子:

运行结果:

在下例中,<code>setjmp</code>被用于包住一个例外处理,类似<code>try</code>。<code>longjmp</code>调用类似于<code>throw</code>语句,允许一个异常返回给<code>setjmp</code>一个异常值。

calling first

calling second

entering second

second failed with type 3 exception; remapping to type 1.

first failed, exception type 1

except接口在一系列宏指令和函数中包装了setjmp和longjmp,它们一起提供了一个结构化异常处理工具

异常是except_t类型的一个全局或静态变量:

except_t结构只有一个字段,它可以初始化为一个描述异常的字符串,当发生一个未处理的异常时,才把字符串打印出来

异常处理程序处理的是异常的地址。异常必须是全局的或静态的变量,因此它们的地址唯一地标志了它们,异常e由宏指令引发或由函数引发:

处理程序是由try-except和try-finally语句来实例化的,这两个语句用宏指令实现,这两个语句可以处理嵌套异常,也可以管理异常状态的数据

try-except的语法是:

try

  s

except(e1)

  s1

except(e2)

  s2

……

except(en)

  sn

else

  s0

end_try

看下面的代码:

上面的代码没有提供嵌套的处理程序,allocation_handle标志的使用也很麻烦。

把allocation_failed变成一个异常,该异常是在malloc返回一个空指针时由allocate引发:

如果客户调用程序代码想处理这个异常,那么它需要在try-except语句内调用allocate:

try-except语句是用setjmp和longjmp来实现的

try-finally语句的语法是:

finally

如果s没有产生任何异常,那么执行s1,然后继续执行end_try,如果s产生了异常,那么s的执行被中断,控制立即转给s1。s1执行完后,引

起s1执行的异常重新产生,使得它可以由前一个实例化的处理程序来处理。注意:s1是在两种情况中都必须执行的,处理程序可以用reraise宏指令显示

地重新产生异常

接口中的最后一个宏指令是:

return宏指令用在try语句的内部,用来代替return语句

except接口中的宏指令和函数一起维护了一个记录异常状态以及实例化处理结构的堆栈。结构中的字段env就是setjmp和longjmp使用的某个jmp_buf,这个堆栈可以处理嵌套的异常

except_stack指向异常栈顶的异常帧,每个帧的prev字段指向它的前一帧,产生一个异常就是将异常的地址存在exception字段中,并分别在file和line字段中保存异常的附属信息--异常产生的文件以及行号

try从句将一个新的except_frame压入异常栈,并调用setjmp,由raise和reraise调用except_raise填充栈

顶帧的字段exception、file和line,从异常栈中弹出栈顶exception_frame,然后调用longjmp,except从句检查

该帧中的exception字段,决定应该用哪个处理程序。finally从句执行清除代码,并重新产生已弹出的异常帧中存储的异常。

宏指令try、except、else、finally_try一起将try-except语句转化成如下形式的语句:

do {

  creat and push an except_frame

  if(first return from setjmp) {

    s

  } else if (exception is e1) {

    s1

  ……

  } else if (exception is en) {

    sn

  } else {

    s0

  }

  if(an exception occurrend and wasn't handled)

    reraise;

} while(0)

exception_frame的空间分配很简单,在由try开始的do-while主体中的复合语句内部声明一个该类型的局部变量即可:

在try语句内有四种状态,由下面的枚举标识符给出

setjmp的第一个返回值将except_flag设置为except_entered,表示进入try语句,并且将某个异常帧压入异常

栈,except_entered必须为0,因为setjmp首次调用的返回值为0,随后,setjmp的返回值将被设为except_raised,表

示发生了异常,处理程序将except_flag的值设成except_handled,表示处理程序已经对异常进行了处理。

最后实现代码如下:

继续阅读