C语言异常与断言接口与实现
程序中通常会出现三种错误:用户错误、运行期错误以及异常
欢迎关注我的个人博客:www.wuyudong.com, 更多精彩文章与您分享
标准库函数setjmp和longjmp
在C语言中,标准库函数setjmp和longjmp形成了结构化异常工具的基础。简单的说就是setjmp实例化处理程序,而longjmp产生异常
setjmp和longjmp是C语言所独有的,它们部分弥补了C语言有限的转移能力。与刺激的abort()和exit()相比,goto语句看起来是处理异常的更可行方案。不幸的是,goto是本地的:它只能跳到所在函数内部的标号上,而不能将控制权转移到所在程序的任意地点(当然,除非你的所有代码都在main体中)。
为了解决这个限制,C函数库提供了setjmp()和longjmp()函数,它们分别承担非局部标号和goto作用。头文件<setjmp.h>申明了这些函数及同时所需的jmp_buf数据类型。
函数说明:
|
建立本地的 |
|
恢复 |
成员类型:
jmp_buf |
数组类型,例如:struct int[16] 或struct __jmp_buf_tag ,用于保存恢复调用环境所需的信息。 |
jmp_buf 的定义:
typedef struct _jmp_buf { int _jp[_JBLEN+1]; } jmp_buf[1];
这个是 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()标记相应的异常处理程序。
本文地址:http://www.cnblogs.com/archimedes/p/c-exception-assert.html,转载请注明源地址。
一个简单的例子:
#include <stdio.h> #include <setjmp.h> static jmp_buf buf; void second(void) { printf("second\n"); // 打印 longjmp(buf,1); // 跳回setjmp的调用处 - 使得setjmp返回值为1 } void first(void) { second(); printf("first\n"); // 不可能执行到此行 } int main() { if ( ! setjmp(buf) ) { first(); // 进入此行前,setjmp返回0 } else { // 当longjmp跳转回,setjmp返回1,因此进入此行 printf("main\n"); // 打印 } return 0; }
运行结果:
second
main
在下例中,setjmp
被用于包住一个例外处理,类似try
。longjmp
调用类似于throw
语句,允许一个异常返回给setjmp
一个异常值。
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <setjmp.h> void first(void); void second(void); static jmp_buf exception_env; static int exception_type; int main(void) { void *volatile mem_buffer; mem_buffer = NULL; if (setjmp(exception_env)) { /* 如果运行到这将产生一个异常*/ printf("first failed, exception type %d\n", exception_type); } else { printf("calling first\n"); first(); mem_buffer = malloc(300); /* 分配内存 */ printf("%s",strcpy((char*)mem_buffer, "first succeeded!")); /* ... 不会被执行 */ } if (mem_buffer) free((void*) mem_buffer); /* 小心释放内存 */ return 0; } void first(void) { jmp_buf my_env; printf("calling second\n"); memcpy(my_env, exception_env, sizeof(jmp_buf)); switch (setjmp(exception_env)) { case 3: /* 如果运行到这,表示有异常 */ printf("second failed with type 3 exception; remapping to type 1.\n"); exception_type = 1; default: memcpy(exception_env, my_env, sizeof(jmp_buf)); /* restore exception stack */ longjmp(exception_env, exception_type); /* continue handling the exception */ case 0: /* normal, desired operation */ second(); printf("second succeeded\n"); /* not reached */ } memcpy(exception_env, my_env, sizeof(jmp_buf)); /* restore exception stack */ } void second(void) { printf("entering second\n" ); exception_type = 3; longjmp(exception_env, exception_type); printf("leaving second\n"); }
运行结果:
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类型的一个全局或静态变量:
#ifndef EXCEPT_INCLUDED #define EXCEPT_INCLUDED #include <setjmp.h> #define T Except_T typedef struct T { char *reason; } T;
Except_T结构只有一个字段,它可以初始化为一个描述异常的字符串,当发生一个未处理的异常时,才把字符串打印出来
异常处理程序处理的是异常的地址。异常必须是全局的或静态的变量,因此它们的地址唯一地标志了它们,异常e由宏指令引发或由函数引发:
#define RAISE(e) Except_raise(&(e), __FILE__, __LINE__) void Except_raise(const T *e, const char *file,int line);
处理程序是由TRY-EXCEPT和TRY-FINALLY语句来实例化的,这两个语句用宏指令实现,这两个语句可以处理嵌套异常,也可以管理异常状态的数据
TRY-EXCEPT的语法是:
TRY
S
EXCEPT(e1)
S1
EXCEPT(e2)
S2
……
EXCEPT(en)
Sn
ELSE
S0
END_TRY
看下面的代码:
int Allocation_handle = 0; jmp_buf Allocate_Failed; void *allocate(unsigned n) { void *new = malloc(n); if(new) return new; if(Allocation_handle) longjmp(Allocate_Failed, 1); assert(0); } char *buf; Allocation_handle = 1; if(setjmp(Allocate_Failed)) { fprintf(stderr, "cound't allocate the buff\n"); exit(EXIT_FAILURE); } buf = allocate(4096); Allocation_handle = 0;
上面的代码没有提供嵌套的处理程序,Allocation_handle标志的使用也很麻烦。
把Allocation_Failed变成一个异常,该异常是在malloc返回一个空指针时由allocate引发:
Except_T Allocate_Failed = {"Allocation failed"}; void *allocate(unsigned n) { void *new = malloc(n); if(new) return new; RAISE(Allocation_Failed); assert(0); }
如果客户调用程序代码想处理这个异常,那么它需要在TRY-EXCEPT语句内调用allocate:
extern Except_T Allocate_Failed; char *buf; TRY buf = allocate(4096); EXCEPT(Allocate_Failed) fprintf(stderr, "could't allocate the buff\n"); exit(EXIT_FAILURE); END_TRY;
TRY-EXCEPT语句是用setjmp和longjmp来实现的
TRY-FINALLY语句的语法是:
TRY
S
FINALLY
S1
END_TRY
如果S没有产生任何异常,那么执行S1,然后继续执行END_TRY,如果S产生了异常,那么S的执行被中断,控制立即转给S1。S1执行完后,引起S1执行的异常重新产生,使得它可以由前一个实例化的处理程序来处理。注意:S1是在两种情况中都必须执行的,处理程序可以用RERAISE宏指令显示地重新产生异常
#define RERAISE Except_raise(Except_frame.exception, \ Except_frame.file, Except_frame.line)
接口中的最后一个宏指令是:
#define RETURN switch (Except_stack = Except_stack->prev,0) default: return
RETURN宏指令用在TRY语句的内部,用来代替return语句
实现
Except接口中的宏指令和函数一起维护了一个记录异常状态以及实例化处理结构的堆栈。结构中的字段env就是setjmp和longjmp使用的某个jmp_buf,这个堆栈可以处理嵌套的异常
typedef struct Except_Frame Except_Frame; struct Except_Frame { Except_Frame *prev; jmp_buf env; const char *file; int line; const T *exception; }; extern Except_Frame *Except_stack;
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主体中的复合语句内部声明一个该类型的局部变量即可:
#define TRY do { \ volatile int Except_flag; \ Except_Frame Except_frame; \ Except_frame.prev = Except_stack; \ Except_stack = &Except_frame; \ Except_flag = setjmp(Except_frame.env); \ if (Except_flag == Except_entered) {
在TRY语句内有四种状态,由下面的枚举标识符给出
enum { Except_entered=0, Except_raised, Except_handled, Except_finalized };
setjmp的第一个返回值将Except_flag设置为Except_entered,表示进入TRY语句,并且将某个异常帧压入异常栈,Except_entered必须为0,因为setjmp首次调用的返回值为0,随后,setjmp的返回值将被设为Except_raised,表示发生了异常,处理程序将Except_flag的值设成Except_handled,表示处理程序已经对异常进行了处理。
#define TRY do { \ volatile int Except_flag; \ Except_Frame Except_frame; \ Except_frame.prev = Except_stack; \ Except_stack = &Except_frame; \ Except_flag = setjmp(Except_frame.env); \ if (Except_flag == Except_entered) { #define EXCEPT(e) \ if (Except_flag == Except_entered) Except_stack = Except_stack->prev; \ } else if (Except_frame.exception == &(e)) { \ Except_flag = Except_handled; #define ELSE \ if (Except_flag == Except_entered) Except_stack = Except_stack->prev; \ } else { \ Except_flag = Except_handled; #define FINALLY \ if (Except_flag == Except_entered) Except_stack = Except_stack->prev; \ } { \ if (Except_flag == Except_entered) \ Except_flag = Except_finalized; #define END_TRY \ if (Except_flag == Except_entered) Except_stack = Except_stack->prev; \ } if (Except_flag == Except_raised) RERAISE; \ } while (0)
最后实现代码如下:
#include <stdlib.h> #include <stdio.h> #include "assert.h" #include "except.h" #define T Except_T #ifdef WIN32 __declspec(thread) #endif Except_Frame *Except_stack = NULL; void Except_raise(const T *e, const char *file, int line) { Except_Frame *p = Except_stack; assert(e); if (p == NULL) { fprintf(stderr, "Uncaught exception"); if (e->reason) fprintf(stderr, " %s", e->reason); else fprintf(stderr, " at 0x%p", e); if (file && line > 0) fprintf(stderr, " raised at %s:%d\n", file, line); fprintf(stderr, "aborting...\n"); fflush(stderr); abort(); } p->exception = e; p->file = file; p->line = line; Except_stack = Except_stack->prev; longjmp(p->env, Except_raised); }
参考资料
《C语言接口与实现--创建可重用软件的技术》