处理C与C++中的异常问题的步骤

处理C与C++中的异常问题的步骤

1. Microsoft对异常处理方法的扩展

前次,我概述了异常的分类和C标准库支持的处理方法。这次讨论Microsoft对这些方法的扩展:结构化异常处理(SEH)和Microsoft Foundation Class MFC)异常处理。SEHCC++都有效,MFC异常体系只对C++有效。

1.1 机构化异常处理

机构化异常处理是Windows提供的服务功能并对所有语言写的程序有效。在Visual C++中,Microsoft封装和简化了这些服务(通过非标准的关键字和库程序)。Windows平台的其它编译器可能选择不同的方式来到达相似的结果。在这个专栏中,名词“Structured Exception Handling”和“SEH”专指Visual C++Windows异常服务的封装。

1.2 关键字

为了支持SEHMicorsoft用四个新关键字扩展了CC++语言:

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中搜索到33GetExceptionCode,两个_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 异常处理函数“看到”异常,并且有机会捕获它(阶段34)。取决于处理函数的意愿,异常将或者恢复或者终止。(阶段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函数。此函数返回一个int0表明是从正常流程进入的,其它值表明在异常终止时进入的。

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))