处理C与C++中的异常问题的步骤
处理C与C++中的异常问题的步骤
1. Microsoft对异常处理方法的扩展
前次,我概述了异常的分类和C标准库支持的处理方法。这次讨论Microsoft对这些方法的扩展:结构化异常处理(SEH)和Microsoft Foundation Class (MFC)异常处理。SEH对C和C++都有效,MFC异常体系只对C++有效。
1.1 机构化异常处理
机构化异常处理是Windows提供的服务功能并对所有语言写的程序有效。在Visual C++中,Microsoft封装和简化了这些服务(通过非标准的关键字和库程序)。Windows平台的其它编译器可能选择不同的方式来到达相似的结果。在这个专栏中,名词“Structured Exception Handling”和“SEH”专指Visual C++对Windows异常服务的封装。
1.2 关键字
为了支持SEH,Micorsoft用四个新关键字扩展了C和C++语言:
l __except
l __finally
l __leave
l __try
因为这是非标关键字,必须打开扩展选项后再编译(关掉/Fa)。
为什么这些关键字带下划线?C++标准(条款17.4.3.1.2,“Global names”)规定:
下列名字和函数总是保留给编译器:
l 所有带双下划线(__)或以一个下划线加一个大写字母开始的名字保留给编译器随意使用。
l 所有以一个下划线开始的名字保留给编译器作全局名称用。
C标准有类似的申明。
既然SEH的关键字符合上面的规则,Microsoft就有权这样使用它们。这也表明,你不被允许在自己的程序中使用保留的名字。你必须避免定义名字类似__MYHEADER_H__或_FatalError的标识符。
有趣而又不幸地,Visual C++的application wizards产生的源代码使用了保留的标识符。例如,如果你用ATL COM App Wizard生成一个新的service,结果框架代码定义了如_Handler和_twinMain的名字--标准所说的你的程序不能使用的保留名称。
要减少这个不合规定行为,你当然可以手工更改这些名称。还好,这些有疑问的名字都是类的私有变量,在类的定义外面是不可见的,在.h和.cpp中进行全局替换是可行的。不幸的是,有一个函数(_twinMain)和一个对象(_Module)被申明了extern,也就是说程序的其它部分会假定你使用了这些名字。(事实上,Visual C++库libc.lib在连接时需要名字_twinMain可用。)
我建议你保留Wizard生成的名字,不要在你自己的代码中定义这样的名字就可以了。另外,你应该将所有不合标准的定义写入文档并留给程序的维护人员;记住,Visual C++以后的版本(和现有的其它C++编译器)可能以另外的方式使用这些名字,从而破坏了你的代码。
1.3 标识符
Microsoft也在非标头文件excpt.h中定义了几个SEH的标识符,并且包含入windows.h中。在其内部,定义了:
l 供__except的过滤表达式使用的过滤结果宏。
l Win32对象和函数的别名宏,用于查询异常信息和状态。
l 伪关键字宏,和前面谈到的四个关键字有着相同名字和含义,但没有下划线。(例如,宏leave对应SEH关键字__leave。)
Microsoft用这些宏令我抓狂。他们对同一个函数了定义多个别名。例如,excpt.h有如下申明和定义:
unsigned long __cdecl _exception_code(void);
#define GetExceptionCode _exception_code
#define exception_code _exception_code
也就是说,你可以用三种方法调用同一函数。你用哪个?并且,这些别名会如你所期望地被维护吗?
在Microsoft的文档中,它看起来偏爱GetExceptionCode,它的名字和其它全局Windows API函数风格一致。我在MSDN中搜索到33处GetExceptionCode,两个_exception_code,而exception_code个数为0。根据Microsoft的引导,推荐使用GetExceptionCode及类似名称的其它函数。
因为_exception_code的两个别名是宏,所以你不能再使用同样的名字了。我曾经犯过这个错,当我在为这个专栏写例程的时候。我定义了一个局部对象叫exception_code(大概是吧)。实际上我就是定义了一个局部对象叫_exception_code,这是我无意中使用的宏exception_code展开的结果。当我一想到是这个问题,解决方案就是简单地将我的对象名字从exception_code改为code。
最后,excpt.h定义了一个特别的宏--“try”--已经成为C++真正的关键字的东西。这意味着你不能在包含了excpt.h的编译单元中简单地混合SEH和标准C++的异常块,除非你愿意#undef这个try宏。当这样undef而露出真正的try关键字时,要冒搞乱SEH的维护人员大脑的危险。另一方面,精通标准C++的程序员会将try理解为一个关键字而不是宏。
我认为,包含一个头文件(即使是象excpt.h这样的非标头文件)不应该改变符合语言标准的代码的行为。我更坚持掩盖或重定义掉语言标准定义的关键字是个坏习惯。我建议:#undef try,同样不使用其它的伪关键字宏,直接使用真正的关键字(如__try)。
1.4 语法
最基本的SEH语法是try块。如下形式:
__try compound-statement handler
处理体:
__except ( filter-expression ) compound-statement
或:
__finally compound-statement
完整一点看,try块如下:
__try
{
...
}
__except(filter-expression)
{
...
}
或:
__try
{
...
}
__finally
{
...
}
在__try里面你必须使用一个leave语句:
__try
{
...
__leave;
...
}
在更大的程序块中,一个try块被认为是个单条语句:
if (x)
{
__try
{
...
}
__finally
{
...
}
}
等价于:
if (x)
__try
{
...
}
__finally
{
...
}
其它注意点:
l 在给定的try块中你必须有一个正确的异常处理函数。
l 所有的语句必须合并。即使只有一条语句跟在__try、__except或__finally后面也必须将它放入{}中。
l 在异常处理函数中,相应的过滤表达式必须有一个或能转换为一个int型的值。
1.5 基本语意
上次我列举了异常生命期的5个阶段。在SEH体系下,这些阶段实现如下:
l 操作系统上报了一个硬件错误或检测到了一个软件错误,或用户代码检测到一个错误(阶段1)。
l (通常是由用户调用Win32函数RasieException启动,)操作系统产生并触发一个异常对象(阶段2)。这个对象是一个结构,其属性对异常处理函数可见。
l 异常处理函数“看到”异常,并且有机会捕获它(阶段3和4)。取决于处理函数的意愿,异常将或者恢复或者终止。(阶段5)。
一个简单的例子:
int filter(void)
{
/* Stage 4 */
}
int main(void)
{
__try
{
if (some_error) /* Stage 1 */
RaiseException(...); /* Stage 2 */
/* Stage 5 of resuming exception */
}
__except(filter()) /* Stage 3 */
{
/* Stage 5 of terminating exception */
}
return 0;
}
Microsoft调用定义在__except中的异常处理函数,和定义在__finally中的终止函数。
一旦异常被触发,由__except开始的异常处理函数被异常发生点顺函数调用链向外面询问。每个被发现的异常处理函数,其过滤表达式都被求值。每次求值后发生什么取决于其返回结果。
excpt.h定义了3个过滤结果的宏,都是int型的:
l EXCEPTION_CONTINUE_EXECUTION = -1
l EXCEPTION_CONTINUE_SEARCH = 0
l EXCEPTION_EXECUTE_HANDLER = 1
前面我说过,过滤表达式必须兼容int型,所以它们和这3个宏的值匹配。这个说法太保守了:我的经验显示Visual C++接受的过滤表达式可以具有所有的整型、指针型、结构、数组甚至是void型!(但我在尝试浮点指针时遇到了编译错误。)
更进一步,所有求出的值看来都有效(至少对整型如此)。所有非零且符号位为0的值效果相当于EXCEPTION_EXECUTE_HANDLER,而符号位为1的相当于EXCEPTION_CONTINUE_EXECUTION。这大概是按位取模的结果。
如果一个异常处理函数的过滤求值结果是EXCEPTION_CONTINUE_SEARCH,这个处理函数拒绝捕获异常,将继续搜索下一个异常处理函数。
通过由过滤表达式产生一个非EXCEPTION_CONTINUE_SEARCH来捕获异常,一旦捕获,程序就恢复。怎么恢复仍然由过滤表达式的值决定:
l EXCEPTION_CONTINUE_EXECUTION:表现为恢复异常。从发生异常处下面开始执行。异常处理函数本身的代码不执行。
l EXCEPTION_EXECUTE_HANDLER:表现为终止异常。从异常发生处开始退栈,一路上所遇到终止函数都被执行。栈退到捕获异常的处理函数所在的一级为止。进入处理函数体并执行。
如名所示,终止处理函数(以__finally开始的代码)在终止异常时被调用。里面是clean up代码,它们就象C标准库中的atexit()函数和C++的析构函数。终止处理函数在正常执行流程也会进入,就象不是捕获型代码。相反,异常处理函数总表现为捕获型:它们只在其过滤表达式求值为EXCEPTION_EXECUTE_HANDLER时才进入。
终止处理函数并不明确知道自己是从正常流程进入的还是在一个try块异常终止时进入的。要判断这点,可以调用AbnormalTermination函数。此函数返回一个int,0表明是从正常流程进入的,其它值表明在异常终止时进入的。
AbnormalTermination实际上是个指向_abnormal_termination()的宏。Visual C++将_abnormal_termination()设计为环境敏感的函数,就象一个关键字。你不能随便调用这个函数,只能在终止处理函数中调用。这意味着你不能在终止处理函数中调用一个中间函数,再在此中间函数中调用_abnormal_termination(),这样做会得到一个编译期错误。
1.6 例程
下面的C例子显示了不同的过滤表达式值和处理函数本身类型的相互作用。第一个版本是个小的完整程序,以后的版本都在它前面一个上有小小的改动。所有的版本都自解释的,你能看清流程和行为。
程序通过RaiseException()触发一个异常对象。RaiseException()函数的第一个参数是异常的代码,类型是32位无符号整型(DWORD);Microsoft为用户自定义的错误保留了[0xE0000000,0xEFFFFFFF]的范围。其它参数一般填0。
这里使用的异常过滤器很简单。实际使用中,大概要调用GetExceptionCode()和GetExceptionInformation()来查询异常对象的属性。
1.7 Version #1: Terminating Exception
用Visual C++生成一个空的Win32控制台程序,命名为SEH_test,选项为默认。将下列C源码加入工程文件:
#include <stdio.h>
#include "windows.h"
#define filter(level, status) /
( /
printf("%s:%*sfilter => %s/n", /
#level, (int) (2 * (level)), "", #status), /
(status) /
)
#define termination_trace(level) /
printf("%s:%*shandling %snormal termination/n", /
#level, (int) (2 * (level)), "", /
AbnormalTermination() ? "ab" : "")
static void trace(int level, char const *message)
{
printf("%d:%*s%s/n", level, 2 * level, "", message);
}
extern int main(void)
{
DWORD const code = 0xE0000001;
trace(0, "before first try");
__try
{
trace(1, "try");
__try
{
trace(2, "try");
__try
{
trace(3, "try");
__try
{
trace(4, "try");
trace(4, "raising exception");
RaiseException(code, 0, 0, 0);
trace(4, "after exception");
}
__finally
{
termination_trace(4);
}
end_4:
trace(3, "continuation");
}
__except(filter(3, EXCEPTION_CONTINUE_SEARCH))