一直以来对“异常”以及相关的知识很模糊,这次做个整理,回答几个基本问题以帮助对“异常”的理解:
C语言本身对异常处理比较少,当异常发生时,往往有以下方式:
1. exit()与abort()。
使用C库函数exit()和abort() 强行终止程序运行。
exit() - 通常表示程序正常退出。exit()退出之前会做一些清理工作,比如销毁static变量,刷一下缓冲区(不知道是否指文件系统缓冲区?),关闭IO通道,然后结束程序。用户还可通过at_exixt(your_exit_fun)设置自己的退出函数,程序exit()时会调用,通过your_exit_fun()做自定义的清理工作,比如把文件写回磁盘。
abort() - 该函数被调用通常表示出现了无法处理的异常,直接终止程序。它不像exit()那样做清理工作。
2.使用assert(断言)宏调用,位于头文件
3.使用errno全局变量,由C运行时库函数提供,位于头文件
4.使用goto语句,当出错时跳转。
5.使用setjmp, longjmp进行异常处理。
不像goto只能实现函数内跳转,setjmp / longjmp一起用可以跨函数跳转。
setjump, longjmp的原理是先通过setjump设置一个跳转点,其实就记录下跳转点的栈信息,然后通过longjmp 来恢复该栈点信息,从而跳转到setjump事先定义的跳转点上执行。
函数定义:
int setjmp(jmp_buf env);
void longjmp(jmp_buf env, int val);
为什么setjmp会返回两次?
看它的使用,或者就理解了:
#include
#include
#include
jmp_buf jmpbuffer;
void raise_exception(void)
{
printf("raise_exception()\n");
longjmp(jmpbuffer,1);
}
int main(int argc, char *argv[])
{
if( setjmp(jmpbuffer) ==0 ) //跳转点,等一会跳回这里
{
raise_exception();
}
else //return from calling longjmp, it returns 1
{
printf("catch exception here.\n");
}
return 0;
}
输出:
raise_exception()
catch exception here.
这类似于LINUX的fork()函数,返回两次,但父子进程返回值不同(难道fork()就是这么实现的?)。
通过setjmp / longjmp甚至可用来实现try / catch这样的异常处理。文献<< Exceptions in C withLongjmp and Setjmp >> 里就展示了这种做法。
上述C语言异常中提到abort(),exit(),C++中也有类似的函数终止程序,即terminate(),它最终调用abort()。同样,它不负责做清理工作。
在C++中,清理工作显的更为普遍:C++引入了类,往往希望类对象析构时能做些事情。而无论是C版的abort(),exit() ,还是C++版的terminate()都无法做到这点。
C++引入的异常处理机制使得异常发生时,函数内本地对象可以被析构。其实现机制称为“栈回退”(stackunwind)。当一个函数抛出异常时,在栈中先做个标记,等到该异常被捕捉并处理时,就能顺滕摸瓜找到异常抛出处,并析构异常抛出之前的的对象。对栈回退原理感兴趣的读者可以参阅《C++栈回退》。
以下是一个简单的例子:
//省略了必要的头文件
class A
{
public:
~A(){printf("~A()\n");}
};
void fun()
{
A a1;
throw "fun() exception";
A a2;
}
int main(int argc, char *argv[])
{
try
{
fun();
printf("would not come here.\n");
}
catch (...)
{
printf("catch exception here.\n");
}
return 0;
}
输出:
~A()
catch exception here.
可看到当异常被处理前,A()中a1被析构,而a2这句没有执行到,所以也不存在析构被调用。
需要注意的是,只有异常被处理了,对应的异常抛出函数才能析构其本地对象。如果一个异常没有被处理,最终系统将调用terminate()终止程序,就像上面说的,这时本地变量将没有机会析构。
我们在编写软件时会发生异常,比如参数不合法,逻辑错误,程序员都可以自定义异常类型;而硬件其实也会发生异常并由CPU抛出,比如除0操作,内存非法访问等。而程序员只关心软件,能不能在软件中统一的捕获所有软件与硬件异常呢?
答案是肯定的。根据MSDN的解释,Windows结构化异常机制就是用来统一软、硬件异常处理机制的,同时还统一了内核与用户态异常的处理机制。简单理解就是对WINDOWS系统上所有异常来个大一统。
那这和C++标准异常又有什么区别与联系?
我们知道C++中定义了关键字try/catch来处理C++标准异常,而C++标准相对于操作来说,是上层的环境,就像CRT与OS的关系一样,CRT的实现依赖于更底层的OSAPI。所以C++标准异常是基于WINDOWS结构化异常上实现的。从Windows结构化异常这层来看,每种异常都有一个代码号。通过C++throw抛出的标准异常代码号总是0xe06d7363。
WINDOWS结构化异常的处理使用 __try/__except(微软自定义的关键字会加两个下划线做前缀)。
注意:__try{}只保护当前线程,如果在__try{}中又创建了新线程,则新的线程不受保护。
在一些项目中,经常出现C/C++代码混用的情况。C代码调用了WINDOWS API,C++又实现与项目相关的逻辑。这样的环境中,WINDOWS结构化异常和C++标准异常都会出现,一个正确的异常捕捉方案应该能做到:
1. 能捕获C++异常,并且异常处理时,C++本地对象能够被析构。
2. 能捕获其它WINDOWS结构化异常(即其它没有被条件一捕获的异常)。
让我们来看看几种方案:
即然说到在WINDOWS平台上,__try/ __except是更底层的,它能同时捕获结构化异常和C++标准异常。只可惜,__try/ __except的栈回退无法正确析构C++对象,因此,用它来代替try/catch处理C++异常不是好方案。
事实上,在VS2010中,如果在__try/ __except块中定义了C++对象,会收到编译错误:
error C2712: Cannot use __try in functions that requireobject unwinding
结论:方案一不能满足条件一。
方案二:使用try / catch 与SetUnhandledException(myFilter)
本方案使用try/catch来捕捉C++异常,利用try/catch处理异常时的栈回退机制,C++对象得以析构,从而满足条件一;
WINDOWS结构化异常因为不会被try/catch捕获,会变成“未处理”异常,利用API SetUnhandledExceptionFilter(myFilter) 捕获其它未处理异常,并在myFilter中处理。
该方案能同时满足上述两个条件,算较好解决问题了。
仍然有问题?
可是,在实际应用,仍然发现有些异常,既没有被try/catch捕获,也没有进入myFilter被处理。一个常见的例子就是CRT非法参数异常。
本来,CRT非法参数异常是能被 SetUnhandledExceptionFilter(myFilter) 捕获的,但自从VS2005开始,这一行为发生了改变:http://blog.kalmbachnet.de/?postid=75
导致很多情况下我们的SetUnhandledExceptionFilter()不起作用。
在VS2010上,我们观察一下CRTinvalid parameter异常没被捕获的情况:
//省略了必要头文件
void raise_invalid_parameter_exception()
{
char *p=0;
printf(p);
}
LONG WINAPI myFilter(struct _EXCEPTION_POINTERS *pExp)
{
printf("myFilter\n");
return EXCEPTION_EXECUTE_HANDLER;
}
int main(int argc, char *argv[])
{
::SetUnhandledExceptionFilter(myFilter);
try
{
raise_invalid_parameter_exception();
}
catch (...)
{
printf("catch you");
}
printf("finished.\n");
return 0;
}
运行,程序弹出0xc0000417错误,该错误代码定义在ntstatus.h中,即“CRT invalid parameter”。
此次运行, catch{}块也没有进入,可见try/catch没能捕捉该异常,而myFilter()也没有进入,main()函数的"finished"也没打印了。
究其原因,是因为CRT也调用了SetUnhandledExceptionFilter(NULL)来处理一些未被处理的CRT异常,而CRT的NULL filter覆盖了用户自定义的filter。
方案二的修正:追加CRTInvalid Parameter异常捕获器
对于CRT Invalid Parameter异常,用户需要单独调用_set_invalid_parameter_handler (yourHandler)来专门捕获该异常,若用户没有该设置,则系统默认调用SetUnhandledExceptionFilter(NULL) 来处理,最终调用abort()结束程序。
此外,还有其它类似的异常需要单独捕获,比如:
- 纯虚函数调用异常。
- vector访问越界。不过在VS2010上,这一异常可以被SetUnhandledExceptionFilter捕获。
如果不想用SetUnhandledExceptionFilter(),可以打开编译选项,让try/catch支持捕获Windows结构化异常:
VS2010中, Project -> [Project Name] Properties...-> Configuration Properties -> C/C++ -> Code Generation -> Enable C++ Exceptions, 选择Yes with SEH Exception (/EHa)。
这里几个选项很令人费解,这里就不展开了。
只是,实验表明,CRT Invalid Parameter异常仍然无法被try/catch捕获。
方案二与方案三作用类似,都可以捕获C++异常与Windows结构化异常,只是CRT的一些异常诸如非法参数(Invalid Parameter),纯虚函数调用,vector访问越界(VS2010上没问题)等,仍然无法被try/catch或是SetUnhandledExceptionFilter()捕获。还需要针对某些特殊异常追加专门处理器,最主要的就是CRT Invalid Parameter 异常,需要_set_invalid_parameter_handler (yourHandler) 特别捕获。
C语言异常处理: http://www.cnblogs.com/vimsk/archive/2010/12/11/1901698.html
<
Exceptions in C with Longjmp and Setjmp: http://www.di.unipi.it/~nids/docs/longjump_try_trow_catch.html
C++栈回退:http://baiy.cn/doc/cpp/inside_exception.htm#%E6%A0%88%E5%9B%9E%E9%80%80%EF%BC%88Stack_Unwind%EF%BC%89%E6%9C%BA%E5%88%B6
Structured Exception Handling: http://msdn.microsoft.com/en-us/library/windows/desktop/ms680657%28v=vs.85%29.aspx
"SetUnhandledExceptionFilter" and VC8: http://blog.kalmbachnet.de/?postid=75