异常的概念
在进行程序设计时,错误的产生是不可避免的,如何处理错误?把错误交给谁去处理?程序又该如何从错误中恢复?这是任何程序设计语言都要解决的问题。
所谓错误,是在程序运行过程中发生的异常事件,比如除0溢出、数组越界、文件找不到等,这些事件的发生将阻止程序的正常运行。为了加强程序的鲁棒性(强壮性,robust),程序设计时,必须考虑到可能发生的异常事件并做出相应的处理。
在C语言中,通过使用if语句来判断是否出现了错误,同时,调用函数通过被调用函数的返回值感知在被调用函数中产生的错误事件并进行处理。
但是,这种错误处理机制会导致不少问题,因为在很多情况下需要知道错误产生的内部细节。
通常,用全局变量Errno来存储一个异常事件的类型,这容易导致误用,因为一个Errno的值有可能在被处理前被另外的错误覆盖掉。此外,即使最优美的C语言程序,为了处理异常情况,也常常求助于goto语句。
没有错误处理的程序:
{
openTheFile;
determine its size;
allocate that much memory;
read-file
closeTheFile;
}
以常规方法处理错误:
openFiles;
if (theFilesOpen)
{
determine the length of the file;
if (gotTheFileLength)
{
allocate that much memory;
if (gotEnoughMemory)
{
read the file into memory;
if (readFailed) errorCode=-1;
else errorCode = -2;
}
else errorCode=-3;
}
else errorCode=-4 ;
}
else errorCode=-5;
以常规方法处理错误存在的问题:
观察前面的程序,大家会发现大部分精力花在出错处理上了;
只把能够想到的错误考虑到,对以外的情况无法处理;
程序可读性差,大量的错误处理代码混杂在程序中 ;
出错返回信息量太少,无法更确切的了解错误状况或原因。
用异常的形式处理错误:
{
try
{
openTheFile;
determine its size;
allocate that much memory;
read-File;
closeTheFile;
}
catch(fileopenFailed) { dosomething; }
catch(sizeDetermineFailed) { dosomething; }
catch(memoryAllocateFailed) { dosomething; }
catch(readFailed) { dosomething; }
catch(fileCloseFailed) { dosomething; }
finally { dosomething; }
}
异常机制的优点:
把错误处理代码从常规代码中分离出来;
按错误类型和差别分组(类Exception,派生);
对无法预测的错误的捕获和处理(基类) ;
克服了传统方法的错误信息有限的问题(getMessage);
把错误传播给调用堆栈(比较:全局变量,返回值)。
什么情况下使用异常机制?
当方法因为自身无法控制的原因而不能完成其任务;
文件不存在,网络连接无法建立…… ;
处理在方法、类库、类中抛出的异常;
如FileInputStream.read产生IOException ;
在大的项目中采用统一的方式处理错误时;
如编写一个文字处理器;
异常应该是不经常发生但却可能发生的故障;
一定发生的事件不应该用异常机制来处理;
异常处理用于使系统从故障中恢复;
提示信息/不产生无效的结果/释放资源。
不同的异常处理策略:
关键性应用(处理所有异常);
实验软件(可以忽略许多异常);
处理异常时的注意事项;
终止程序会导致资源泄漏,利用异常处理释放资源;
尽可能近地处理异常,这样程序清晰易读;
能在局部处理的错误不要使用异常机制;
异常机制的处理比正常处理效率低。
异常机制的关键步骤
try {…}
定义可能产生异常的代码段
catch (Etype e) {…}
用于捕获一个异常
finally {…}
用于做统一的事后处理,如释放资源
throw e;
用于抛出一个异常
throws Etype1, Etype2 ……
用于声明方法可能抛出的异常类型
C语言实现异常处理机制
不管是在c++还是在java中,异常都被认为是一种很优雅的处理错误的机制,而如果想在c语言中使用异常就比较麻烦。但是我们仍然可以使用c语言中强大的setjmp和longjmp函数实现类似于c++的异常处理机制。
有关c语言中setjmp和longjmp的资料可以参考:
C语言中一种更优雅的异常处理机制:http://blog.youkuaiyun.com/hello_wyq/archive/2006/06/23/826312.aspx
全面了解setjmp与longjmp的使用:http://blog.youkuaiyun.com/hello_wyq/archive/2006/06/16/804040.aspx
基本原理
结合setjmp,将当前的环境变量打包为frame(定义的一个结构名)压到一个异常堆栈(自定义的结构体)中,如果程序段正常运行,将此frame弹出,而如果程序出错,将异常栈的顶部元素弹出,根据这个栈顶元素的frame中保存的环境变量,通过setjmp将环境恢复,然后执行某个错误处理函数,而如果没有相应错误处理函数,重新弹出新的栈顶元素,以跳到更外层的setjmp块进行处理。
主要代码分析
此异常机制的实现大量应用了宏,以实现c++和java中异常处理的语法效果。如何使用见下面的如何使用部分。
try部分,作用见注释:c 代码
-
-
- #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) \
- {
最重要的部分要数except_raise函数,检查异常是否被处理,如果未被处理,重新从异常栈中弹出新的frame,以跳到更外层的异常处理块。
catch(e)也是宏,检查当前的frame是否和这个catch块中的e对应,如果对应的话,执行下面的部分进行处理。
因为所有except_frame全部放在栈上,因此可以说这个except_stack利用了程序自动产生的stack机制,只要正确地改变Except_stack的值就可以了,不必再考虑分配的except_frame的释放问题,空间的分配释放全由程序自动生成的stack管理。
如何使用
使用这个异常机制的代码,如下
c 代码
- try{
- S;
- }catch(e1){
- S1;
- }catch(e2){
- S2;
- }else_catch{
- S3;
- }end_try;
此相当于c++中的:
- try{
- S;
- }catch(e1){
- S1;
- }catch(e2){
- S2;
- }catch(…){
- S3;
- }
当前实现的异常机制也支持finally语句,因此下面的代码:
c代码
- try{
- S;
- }catch(e1){
- S1;
- }finally{
- S2;
- }end_try;
相当于java中的:
java 代码
- try{
- S;
- }catch(e1except e1){
- S1;
- }finally
- S2;
源代码
文件:exception.h
c 代码
- #ifndef __EXCEPTION_H__
- #define __EXCEPTION_H__
-
- #include <stdio.h></stdio.h>
- #include <setjmp.h></setjmp.h>
- #include <assert.h></assert.h>
-
-
- #define T Except_t
- typedef struct Except_t{
- char *reason;
- }Except_t;
-
- typedef struct Except_frame{
- struct Except_frame *prev;
- jmp_buf env;
- const char *file;
- int line;
- const T* exception;
- }Except_frame;
-
- extern Except_frame *Except_stack;
-
-
- enum {EXCEPT_ENTERED=0,EXCEPT_RAISED,
- EXCEPT_HANDLED,EXCEPT_FINALIZED};
-
- #define throw(e) except_raise(&(e),__FILE__,__LINE__)
-
- #define rethrow except_raise(except_frame.exception,\
- except_frame.file,except_frame.line)
-
- void abort_without_exception(const Except_t *e,const char *file,int line);
-
-
- void except_raise(const T *e,const char *file,int line);
-
-
-
- #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 catch(e) \
- if(except_flag == EXCEPT_ENTERED) \
- Except_stack = Except_stack->prev; \
- }else if(except_frame.exception == &(e)){ \
- except_flag = EXCEPT_HANDLED;
-
- #define try_return \
- switch(Except_stack = Except_stack->prev,0) \
- default: return
-
- #define catch_else \
- if(except_flag == EXCEPT_ENTERED) \
- Except_stack = Except_stack->prev; \
- }else{ \
- except_flag = EXCEPT_HANDLED;
-
-
- #define end_try \
- if(except_flag == EXCEPT_ENTERED) \
- Except_stack = Except_stack->prev; \
- } \
- if (except_flag == EXCEPT_RAISED) \
- except_raise(except_frame.exception, \
- except_frame.file,except_frame.line); \
- }while(0)
-
-
- #define finally \
- if(except_flag == EXCEPT_ENTERED) \
- Except_stack = Except_stack->prev; \
- }{ \
- if(except_flag == EXCEPT_ENTERED) \
- except_flag = EXCEPT_FINALIZED;
-
- #undef T
- #endif
文件:exception.c
c 代码
- #include "exception.h"
-
- Except_frame *Except_stack = NULL;
-
- void except_raise(const Except_t *e,const char *file,int line)
- {
- Except_frame *p = Except_stack;
-
- assert(e);
- if(p == NULL){
- abort_without_exception(e,file,line);
- }
- p->exception = e;
- p->file = file;
- p->line = line;
- Except_stack = Except_stack->prev;
- longjmp(p->env,EXCEPT_RAISED);
- }
-
- void abort_without_exception(const Except_t *e,const char *file,int line)
- {
- 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();
- }
MSDN中还做了特别的说明,“在C+ +程序中,小心对setjmp和longjmp的使用,因为setjmp和longjmp并不能很好地支持C++中面向对象的语义。因此在C++程序中,使用C++提供的异常处理机制将会更加安全。”虽然说C++能非常好的兼容C,但是这并非是100%的完全兼容。例如,这里就是一个很好的例子,在C++ 程序中,它不能很好地与setjmp和longjmp和平共处。在后面的一些文章中,有关专门讨论C++如何兼容支持C语言中的异常处理机制时,会做详细深入的研究,这里暂且跳过。