转的一篇文章,不知道原始出处。
1. 异常和标准C对它的支持
(前言略)
1.1 异常分类
基于Dr. GUI的建议,我把我的第一个专栏投入到“程序异常”的系列上。我认识到,“exception”这个
术语有些不明确并和上下文相关,尤其是C++标准异常(C++ standard exceptions)和Microsoft的结构化异常
(structured exception handling)。不幸的的是,“异常”一词太常见了,随时出现在语言的标准和常见的
编程文献中。因为不想创造一个新名词,所以我将尽力在此系列的各部分中明确我对“异常”的用法。
Part 1概述通常意义上的异常的性质,和标准C库提供的处理它们的方法。
Part 2纵览Microsoft对这些标准C库方法的扩展:专门的宏和结构化异常处理。
Part 3及其余将致力于标准C++异常处理体系。
(C语言使用者可能在Part2后放弃,但我鼓励你坚持到底;我所提出的许多点子同样适用于C,虽然不是很直接
。)
本质上看,程序异常是指出现了一些很少发生的或出乎意料的状态,通常显示了一个程序错误或要求一个
必须提供的回应。不能满足这个回应经常造成程序功能削弱或死亡,有时导致整个系统和它一起down掉。不幸
的是,试图使用传统的防护方法来编制健壮的代码经常只是将一个问题(意外崩溃)换成了另外一个问题(更
混乱的设计和代码)。
太多的程序员认为这个交换抵不上程序意外崩溃时造成的烦恼,于是选择了生活在危险之中。认识到这一
点后,C++标准增加了一个优雅并且基本上不可见的“异常体系”到语言中;就这样,这个方法产生了。如同我
们在Part4的开始部分将要看到的,这个方法大部分情况下很成功,但在很微妙的情况下可能失败。
1.2 异常的生命阶段
在这个系列里,我将展示C和C++处理异常体系运行于异常整个生命期的每一阶段时的不同之处:
阶段1:一个软件错误发生。这个错误也许产生于一个被底层驱动或内核映射为软件错误的硬件响应事件(如被
0除)。
阶段2:错误的原因和性质被一个异常对象携带。这个对象的类型可以简单的整数值到繁杂的C++类对象。
阶段3:你的程序必须检测这个异常对象:或者轮询它的存在,或者由其主动上报。
阶段4:检测代码必须决定如何处理异常。典型的方法分成三类。
a忽略异常对象,并期望别人处理它。
b在这个对象上干些什么,并还允许别人再继续处理它。
c获得异常的全部所有权。
阶段5:既然异常已经处理了,程序通常恢复并继续执行。恢复分成两种:
a恢复异常,从异常发生处继续执行。
b终止异常,从异常被处理处继续执行。
当在程序外面(由运行期库或操作系统)终止异常时,恢复经常是不可能的,程序将异常结束。
我故意忽略了硬件错误事件,因为它们完全是底层平台范围内的事。取而代之,我假定一些软件上的可检
测错误已经发生,并产生了一个处于第一阶段的软件异常对象。
1.3 C标准库异常处理体系
C标准库提供了几个方法来处理异常。它们也全部在标准C++中有效,只是相关的头文件名字变了:老的C标
准头文件<name.h>映射到了新的C++标准头文件<cname>。(头文件名的前缀“C”是个助记符,暗示着这些全是
C库头文件。)
虽然基于向后兼容性,老的C头文件也被C++保留,但我建议你尽可能使用新的头文件。对于绝大部分实际
使用而言,最大的变化是在新的头文件中,申明的函数被包含在命名空间std内。举个例子,C语言使用
#include <stdio.h>
FILE *f = fopen("blarney.txt", "r");
复制代码
在C++中被改成
#include <cstdio>
std::FILE *f = std::fopen("blarney.txt", "r");
复制代码
或更C风格的
#include <cstdio>
using namespace std;
FILE *f = fopen("blarney.txt", "r");
复制代码
不幸的是,Microsoft的Visual C++没有将这些新的头文件包含在命名空间std中,虽然这是C++标准所要求的(
subclause D.5)。除非Visual C++在这些头文件中已经正确地支持了std,我将一直在我的专栏中使用老式的C
风格命名。
(象MIcrosoft这样的运行库卖主这么做是合理的,正确地实现这些C程序库的头文件极可能要求维护和测
试两份完全不同的底层代码,这是不可能受欢迎的也不值得多花力气的工作。)
1.4 无条件终止
仅次于彻底忽略一个异常,大概最容易的异常处理方法是程序自我毁灭。有时,最懒的方法事实上是最正
确的。
在你开始嘲笑以前,应该认识到,一些异常表示的状况是如此严重以致于怎么也不可能合理恢复的。也许
最好的例子就是malloc时返回NULL。如果空闲堆管理程序不能提供可用的连续空间,你程序的健壮性将严重受
损,并且恢复的可能性是渺茫的。
C库头文件<stdlib.h>提供了两个终止程序的函数:abort()和exit()。这两个函数运行于异常生命期的4和
5。它们都不会返回到其调用者中,并都导致程序结束。这样,它们就是结束异常处理的最后一步。
虽然两个函数在概念上是相联系的,但它们的效果不同:
abort():程序异常结束。默认情况下,调用abort()导致运行期诊断和程序自毁。它可能会也可能不会刷新缓
冲区、关闭被打开的文件及删除临时文件,这依赖于你的编译器的具体实现。
exit():文明地结束程序。除了关闭文件和给运行环境返回一个状态码外,exit()还调用了你挂接的atexit()
处理程序。
一般调用abort()处理灾难性的程序故障。因为abort()的默认行为是立即终止程序,你就必须负责在调用
abort()前存储重要数据。(当我们谈论到<signal.h>时,你可以使得abort()自动调用clean up代码。)
相反,exit()执行了挂接在atexit()上的自定义clean up代码。这些代码被按照其挂接的反序执行,你可
以把它们当作虚拟析构器。通过必要的clean up代码,你可以安全地终止程序而没有留下尾巴。例如:
#include <stdio.h>
#include <stdlib.h>
static void atexit_handler_1(void)
{
printf("within ''''atexit_handler_1''''/n");
}
static void atexit_handler_2(void)
{
printf("within ''''atexit_handler_2''''/n");
}
int main(void)
{
atexit(atexit_handler_1);
atexit(atexit_handler_2);
exit(EXIT_SUCCESS);
printf("this line should never appear/n"); return 0;
}
/* When run yields
within ''''atexit_handler_2''''
within ''''atexit_handler_1''''
and returns a success code to calling environment.
*/
复制代码
(注意,即使是程序从main()正常返回而没有明确调用exit(),所挂接的atexit()代码仍然会被调用。)
无论abort()还是exit()都不会返回到它的调用者中,且都将导致程序结束。在这个意义上来说,它们都表
现为终止异常的最后一步。
1.5 有条件地终止
abort()和exit()让你无条件终止程序。你还可以有条件地终止程序。其实现体系是每个程序员所喜爱的诊
断工具:断言,定义于<assert.h>。这个宏的典型实现如下所示:
#if defined NDEBUG
#define assert(condition) ((void) 0)
#else
#define assert(condition) /
_assert((condition), #condition, __FILE__, __LINE__)
#endif
复制代码
如定义体所示,当宏NDEBUG被定义时断言是无行为的,这暗示了它只对调试版本有效。于是,断言条件从不在
非调试版本中被求值,这会造成同样的代码在调试和非调试版本间有奇妙的差异。
/* debug version */
#undef NDEBUG
#include <assert.h>
#include <stdio.h>
int main(void)
{
int i = 0;
assert(++i != 0);
printf("i is %d/n", i);
return 0;
}
/* When run yields
i is 1
*/
现在,通过定义NDEBUG,从debug版变到release版:
/* release version */
#defing NDEBUG
#include <assert.h>
#include <stdio.h>
int main(void)
{
int i = 0;
assert(++i != 0);
printf("i is %d/n", i);
return 0;
}
/* When run yields
i is 0
*/
复制代码
要避免这个差异,必须确保断言表达式的求值不会包含有影响的副作用。
在仅供调试版使用的定义体中,断言变成呼叫_assert()函数。我起了这个名字,而你所用的运行库的实现
可以调用任何它想调用的内部函数。无论它叫什么,这个函数通常有以下形式:
void _assert(int test, char const *test_image,
char const *file, int line)
{
if (!test)
{
printf("Assertion failed: %s, file %s, line %d/n",
test_image, file, line);
abort();
}
}
复制代码
所以,失败的断言在调用abort()前显示出失败情况的诊断条件、出错的源文件名称和行号。我在这里演示的诊
断机构“printf()”相当粗糙,你所用的运行库的实现可能产生更多的反馈信息。
断言处理了异常的阶段3到5。它们实际上是一个带说明信息的abort()并做了前提条件检查,如果检查失败
,程序中止。一般使用断言调试逻辑错误和绝不可能出现在正确的程序中的情况。
/* ''''f'''' never called by other programs */
static void f(int *p)
{
assert(p != NULL);
/* ... */
}
对比一下逻辑错误和可以存在于正确程序中的运行期错误:
/* ...get file ''''name'''' from user... */
FILE *file = fopen(name, mode);
assert(file != NULL); /* questionable use */
复制代码
这样的错误表示异常情况,但不是bug。对这些运行期异常,断言大概不是个合适的处理方法,你应该用我下面
将介绍的另一个体系来代替。
1.6 非局部的跳转
与刺激的abort()和exit()相比,goto语句看起来是处理异常的更可行方案。不幸的是,goto是本地的:它
只能跳到所在函数内部的标号上,而不能将控制权转移到所在程序的任意地点(当然,除非你的所有代码都在
main体中)。
为了解决这个限制,C函数库提供了setjmp()和longjmp()函数,它们分别承担非局部标号和goto作用。头文件
<setjmp.h>申明了这些函数及同时所需的jmp_buf数据类型。
原理非常简单:
setjmp(j)设置“jump”点,用正确的程序上下文填充jmp_buf对象j。这个上下文包括程序存放位置、栈和框架
指针,其它重要的寄存器和内存数据。当初始化完jump的上下文,setjmp()返回0值。
以后调用longjmp(j,r)的效果就是一个非局部的goto或“长跳转”到由j描述的上下文处(也就是到那原来设置
j的setjmp()处)。当作为长跳转的目标而被调用时,setjmp()返回r或1(如果r设为0的话)。(记住,
setjmp()不能在这种情况时返回0。)
通过有两类返回值,setjmp()让你知道它正在被怎么使用。当设置j时,setjmp()如你期望地执行;但当作
为长跳转的目标时,setjmp() 就从外面“唤醒”它的上下文。你可以用longjmp()来终止异常,用setjmp()标
记相应的异常处理程序。
#include <setjmp.h>
#include <stdio.h>
jmp_buf j;
void raise_exception(void)
{
printf("exception raised/n");
longjmp(j, 1); /* jump to exception handler */
printf("this line should never appear/n");
}
int main(void)
{
if (setjmp(j) == 0)
{
printf("''''setjmp'''' is initializing ''''j''''/n");
raise_exception();
printf("this line should never appear/n");
}
else
{
printf("''''setjmp'''' was just jumped into/n");
/* this code is the exception handler */
}
return 0;
}
/* When run yields:
''''setjmp'''' is initializing ''''j''''
exception raised
''''setjmp'''' was just jumped into
*/
复制代码
那个填充jmp_buf的函数不在调用longjmp()之前返回。否则,存储在jmp_buf中的上下文就有问题了:
jmp_buf j;
void f(void)
{
setjmp(j);
}
int main(void)
{
f();
longjmp(j, 1); /* logic error */
return 0;
}
复制代码
所以,你必须把setjmp()处理成只是到其所在位置的一个非局部跳转。
Longjmp()和setjmp()联合体运行于异常生命期的2和3阶段。longjmp(j,r)产生异常对象r(一个整数),
并且作为返回值传送到setjmp(j)处。实际上,setjmp()函数通报了异常r。
1.7 信号
C函数库也提供了标准的(虽然原始的)“事件”处理包。这个包定义了一组事件和信号,以及标准的方法
来触发和处理它们。这些信号或者表示了一个异常状态或者表示了一个不协调的外部事件;基于所谈论的主题
,我将只集中讨论异常信号。
为了使用这些包,需要包含标准头文件<signal.h>。这个头文件申明了函数raise()和signal(),数据类型
sig_atomic_t,和以SIG开头的信号事件宏。标准要求有六个信号宏,也许你所用的运行库实的现会再附加一些
。这些信号被固定死在<signal.h>中,你不能增加自定义的信号。信号通过调用raise()产生并被处理函数捕获
。运行时体系提供默认处理函数,但你能通过signal()函数安装自己的处理函数。处理函数可以通过
sig_atomic_t类型的对象和外部进行通讯;如类型名所示,对这样的对象的操作是原子操作或者说中断安全的
。
当你挂接信号处理函数时,通常提供一个函数地址,这个的函数必须接受一个整型值(所要处理的信号事
件),并且无返回。这样,信号处理函数有些象setjmp();它们所收到的仅有的异常信息是单个整数:
void handler(int signal_value);
void f(void)
{
signal(SIGFPE, handler); /* register handler */
/* ... */
raise(SIGFPE); /* invoke handler, passing it ''''SIGFPE'''' */
}
复制代码
只可其一地,你可以安装两个特别的处理函数:
signal(SIGxxx,SIG_DFL),为指定的信号挂接系统的缺省处理函数。
signal(SIGxxx,SIG_IGN),告诉系统忽略指定的信号。
signal()函数返回前次挂接的处理函数的地址(表明挂接成功),或返回SIG_ERR(表明挂接失败)。
处理函数被调用表明信号正在试图恢复异常。当然,你可以在处理函数中随意地调用abort()、exit()或
longjmp(),有效地将信号解释为终止异常。有趣的是,abort()自己事实上在内部调用了raise(SIGABRT)。
SIGABRT的缺省处理函数发起了一个诊断并终止程序,当然你可以安装自己的处理函数来改变这个行为。不能改
变的是abort()的终止程序的行为。Abort()理论上的实现如下:
void abort(void)
{
raise(SIGABRT);
exit(EXIT_FAILURE);
}
复制代码
也就是说,即使你的SIGABRT处理函数返回了,abort()仍然中止你的程序。
C语言标准在信号处理函数的行为上增加了一些限制和解释。如果你有C语言标准,我建议你查阅条款
7.7.1.1的细节。(很不幸,C语言和C++语言的标准在Internet都得不到。)
<signal.h>的申明覆盖了异常的整个生存期,从产生到死亡。在标准的C语言运行期库中,它们是最接近于
异常完全解决方案的。
1.8 全局变量
<setjmp.h>和<signal.h>一般使用异常通知体系:当试图通知一个异常事件时唤醒一个处理函数。如果你
更愿意使用轮询体系,C标准库在<errno.h>提供了例子。这个头文件定义了errno及其一些可能的取值。标准要
求这样三个值:EDOM、 ERANGE和EILSEQ,分别适用于域、范围和多字节顺序错误,你的编译器可能又加了些其
它的,它们全以字母“E”开头。
errno,通过由运行库的代码设置它而用户代码查询它的办法将二者联系起来,运行于异常生命期的1到3:
运行库产生异常对象(一个简单的整数),把值拷给errno,然后依赖用户的代码去轮询和检测这个异常。
运行库主要在<math.h>和<stdio.h>的函数中使用errno。errno在程序开始时设为0,函数库程序不会再次
把它设为0。因此,要检测错误,你必须先将errno设为0,再调用运行库程序,调用完后检查errno的值:
#include <errno.h>
#include <math.h>
#include <stdio.h>
int main(void)
{
double x, y, result;
/* ... somehow set ''''x'''' and ''''y'''' ... */
errno = 0;
result = pow(x, y);
if (errno == EDOM)
printf("domain error on x/y pair/n");
else if (errno == ERANGE)
printf("range error on result/n");
else
printf("x to the y = %d/n", (int) result);
return 0;
}
复制代码
注意:errno不一定要绑在一个对象上:
int *_errno_function()
{
static int real_errno = 0;
return &real_errno;
}
#define errno (*_errno_function())
int main(void)
{
errno = 0;
/* ... */
if (errno == EDOM)
/* ... */
}
复制代码
你可以在自己的程序中采用这样的技巧,对errno及其值进行模拟。使用C++的话,你当然可以把这种策略扩展
到类或命名空间的对象和函数上。(实际上,在C++中,这个技巧是Singleton Pattern的基础。)
1.9 返回值和回传参数
象errno这样的异常对象不是没有限制的:
所有相关联的部分必须一致,确保设置和检查同一个对象。
无关的部分可能意外地修改了对象。
如果没有在调用程序前重设对象,或在调用下一步前没有检查它们,你就可能漏了异常。
宏和内部代码中的对象在重名时将掩盖异常对象。
静态对象天生就不是(多)线程安全的。
总之,这些对象很脆弱:你太容易用错它们,编译器没有警告程序却有不可预测的行为。要排除这些不足
,你需要这样的对象:
被两个正确的部分访问--一个产生异常,一个检测异常。
带有一个正确的值。
名字不能被掩盖
1.10 线程安全。
函数返回值满足这些要求,因为它们是无名的临时变量,由函数产生而只能被调用者访问。调用一完成,
调用者就可以检查或拷贝返回值;然后原始的返回对象将消失而不能被重用。又因为是无名的,它不能被掩盖
。
(对于C++,我假设只有右值函数调用表达,也就是说不能返回引用。由于我限定现在只谈论C兼容的技巧
,而C不支持引用,这样的假设是合理的。)
返回值出现在异常生命期的阶段2。在调用和被调用函数的联合体中,这只是完整的异常处理的一部分:
int f()
{
int error;
/* ... */
if (error) /* Stage 1: error occurred */
return -1; /* Stage 2: generate exception object */
/* ... */
}
int main(void)
{
if (f() != 0) /* Stage 3: detect exception */
{
/* Stage 4: handle exception */
}
/* Stage 5: recover */
}
复制代码
返回值是C标准库所喜欢的异常传播方法。看下面的例子:
if ((p = malloc(n)) == NULL)
/* ... */
if ((c = getchar()) == EOF)
/* ... */
if ((ticks = clock()) < 0)
/* ... */
复制代码
注意,典型的C习惯用法:在同一条语句中接收返回值和检测异常。这种压缩表达式重载一个通道(返回值对象
)来携带两个不同的含义:合法的数据值和异常值。代码必须按两条路来解释这个通道,直到知道哪个是正确
的。
这种函数返回值的用法常见于很多语言中,尤其是Microsoft开发的语言无关的Component Object Model
(COM)。COM方法通过返回一类型为HRESULT(特别安排的32位无符号值)的对象提示异常。和刚讨论的例子不同
,COM的返回值只携带状态和异常信息;回传信息通过参数列表中的指针进行。
回传指针和C++的引用型的参数是函数返回值的变形,但有些明显的不同:
你能忽略和丢弃返回值。回传参数则绑定到了相应的实参上,所以不可能完全忽略它们。和返回值相比,参数
在函数和它们的调用者间形成了紧耦合。
通过回传参数可以返回任意个数的值,而通过返回值只能返回一个值。所以回传参数提供了多个返回值。
返回值是临时对象:它们在调用前不存在,并且在调用结束是消失。实参的生命期远长于函数的调用过程。
1.11 小结
这次大概地介绍了异常和标准C对它的传统支持。第二部分,我将研究Microsoft对标准C方法的扩展:特有
的异常处理宏、结构化异常处理或说SEH。我将总结所有C兼容方法(包括SEH)的局限性,并在第三部分拉开
C++异常的序幕
1.1 版本3:恢复异常
接下来,改:
__except(except_filter(3, EXCEPTION_CONTINUE_SEARCH))
为:
__except(except_filter(3, EXCEPTION_CONTINUE_EXECUTION))
重新编译并运行。可以看到这样的输出:
0:before first try
1: try
2: try
3: try
4: try
4: raising exception
3: filter => EXCEPTION_CONTINUE_EXECUTION
4: after exception
4: handling normal termination
3: continuation
2: continuation
2: handling normal termination
1: continuation
0:continuation
因为第三层的异常过滤器已经捕获了异常,第一层的过滤器不会被求值。捕获异常的过滤器求值为
EXCEPTION_CONTINUE_EXECUTION,因此异常被恢复。异常处理函数不会被进入,将从异常发生点正常执行下去
。
1.2 版本4:异常终止
这样的结构:
__try
{
/* ... */
return;
}
或:
__try
{
/* ... */
goto label;
}
__finally
{
/* ... */
}
/* ... */
label:
被认为是try块异常终止。以后调用AbnormalTermination()函数的话将返回非0值,就象异常仍然存在。
要看其效果,改这两行:
trace(4, "raising exception");
RaiseException(exception_code, 0, 0, 0);
为:
trace(4, "exiting try block");
goto end_4;
第4层的try块不是被一个异常结束的,现在是被goto语句结束的。运行结果:
0:before first try
1: try
2: try
3: try
4: try
4: exiting try block
4: handling abnormal termination
3: continuation
2: continuation
2: handling normal termination
1: continuation
0:continuation
第4层的终止处理函数认为它正在处理异常终止,虽然并没有发生过异常。(如果发生过异常的话,我们至
少能从一个异常过滤器的输出信息上看出来的。)
结论:你不能只依赖AbnormalTermination()函数来判断异常是否仍存在。
1.3 版本5:正常终止
如果想正常终止一个try块,也就是想要AbnormalTermination() 函数返回FALSE,应该使用Microsoft特有
的关键字__leave。想验证的话,改:
goto end_4;
为:
__leave;
重新编译并运行,结果是:
0:before first try
1: try
2: try
3: try
4: try
4: exiting try block
4: handling normal termination
3: continuation
2: continuation
2: handling normal termination
1: continuation
0:continuation
和版本4的输出非常接近,除了一点:第4层的终止处理函数现在认为它是在处理正常结束。
1.4 版本6:隐式异常
前面的程序版本处理的都是用户产生的异常。SEH也可以处理Windows自己抛出的异常。
改这行:
trace(4, "exiting try block");
__leave;
为:
trace(4, "implicitly raising exception");
*((char *) 0) = 'x';
这导致Windows的内存操作异常(引用空指针)。接着改:
__except(except_filter(3, EXCEPTION_CONTINUE_EXECUTION))
为:
__except(except_filter(3, EXCEPTION_EXECUTE_HANDLER))
以使程序捕获并处理异常。
执行结果为:
0:before first try
1: try
2: try
3: try
4: try
4: implicitly raising exception
3: filter => EXCEPTION_EXECUTE_HANDLER
4: handling abnormal termination
3: handling exception
2: continuation
2: handling normal termination
1: continuation
0:continuation
如我们所预料,Windows在嵌套层次4中触发了一个异常,并被层次3的异常处理函数捕获。
如果你想知道捕获的精确异常码,可以让异常传到main外面去,就象版本2中做的。为此,改:
__except(except_filter(3, EXCEPTION_EXECUTE_HANDLER))
为:
__except(except_filter(3, EXCEPTION_CONTINUE_SEARCH))
结果对话框在按了“Details”后,显示的信息非常象用户异常。
和版本2的对话框不同是,上次显示了特别的异常码,这次说了“invalid page fault”--更用户友好些
吧。
1.5 C++考虑事项
在所有C兼容异常处理体系中,SEH无疑是最完善和最灵活的(至少在Windows环境下)。具有讽刺意味的,
它也是Windows体系以外的环境中最不灵活的,它将你和特殊的运行平台及Visaul C++源码兼容的编译器牢牢绑
在了一起。
如果只使用C语言,并且不考虑移植到Windows平台以外,SEH很好。但如果使用C++并考虑可移植性,我强
烈建议你使用标准C++异常处理而不用 SEH。你可以在同一个程序中同时使用SEH和标准C++异常处理,只有一个
限制:如果在有SEH try块的函数中定义了一个对象,而这个对象又没有non-trivial(无行为的)析构函数,
编译器会报错。在同一函数中同时使用这样的对象和SEH 的__try,你必须禁掉标准C++异常处理。
(Visual C++默认关掉标准C++异常处理。你可以使用命令行参数/GX或Visual Studio的Project Settings
对话框打开它。)
在以后的文章中,我会在讨论标准C++异常处理时回顾SEH。我想将SEH整合入C++的主流中,通过将结构化
异常及Windows运行库支持映射为C++异常和标准C++运行库支持。
1.6 MFC异常处理
说明:这一节我需要预先引用一点点标准C++异常处理的知识,但要到下次才正式介绍它们。这个提前引用
是不可避免的,也是没什么可惊讶的,因为Microsoft将它们的MFC异常的语法和语义构建在标准C++异常的语法
和语义的基础上。
我到现在为止所讲的异常处理方法对C和C++都有效。在此之外,Microsoft对C++程序还有一个解决方案:
MFC异常处理类和宏。 Microsoft现在认为MFC异常处理体系过时了,并鼓励你尽可能使用标准C++异常处理。然
而Visual C++仍然支持MFC异常类和及宏,所以我将给它个简单介绍。
Microsoft用标准C++异常实现了MFC3.0及以后版本。所以你必须激活标准C++异常才能使用MFC,即使你不
打算显式地使用这些异常。前面说过,你必须禁掉标准C++异常来使用SEH,这也意味着你不能同时使用MFC宏和
SEH。Microsoft明文规定这两个异常体系是互斥的,不能在同一程序中混合使用。
SEH是扩展了编译器关键字集,MFC则定义了一组宏:
TRY
CATCH, AND_CATCH, 和END_CATCH
THROW 和 THROW_LAST
这些宏非常象C++的异常关键字try、catch和throw。
另外,MFC提供了异常类体系。所有名字为CXXXException形式的类都是从抽象类CException派生的。这类
似于标准C++运行库在<setdxcept>中申明的从std::exception开始的派生体系。但,标准C++的关键字可以处理
绝大部分类型的异常对象,而MFC宏只能处理CException的派生类型对象。
对于每个MFC异常类CXXXException,都有一个全局的辅助函数AfxThrowXXXException() ,它构造、初始化
和抛出这个类的对象。你可以用这些辅助函数处理预定义的异常类型,用THROW处理自定义的对象(当然,它们
必须是从 CException派生的)。
基本的设计原则是:
用TRY块包含可能产生异常的代码。
用CATCH检测并处理异常。异常处理函数并不是真的捕获对象,它们其实是捕获了指向异常的指针。MFC靠动态
类型来辨别异常对象。比较一下,SEH靠运行时查询异常码来辨别异常。
可以在一个TRY块上捆绑多个异常处理函数,每个捕获一个C++静态类型不同的对象。第一个处理函数使用宏
CATCH,以后的使用AND_CATCH,用END_CATCH结束处理函数队列。
MFC自己可能触发异常,你也可以显式触发异常(通过THROW或MFC辅助函数)。在异常处理函数内部,可以用
THROW_LAST再次抛出最近一次捕获的异常。
异常一被触发,异常处理函数就将被从里到外进行搜索,和SEH时一样。搜索停止于找到一个类型匹配的异常处
理函数。所有异常都是终止。和SEH不一样,MFC没有终止处理函数,你必须依赖于局部对象的析构函数。
一个小MFC例子,将大部分题目都包括了:
#include <stdio.h>
#include "afxwin.h"
void f()
{
TRY
{
printf("raising memory exception/n");
AfxThrowMemoryException();
printf("this line should never appear/n");
}
CATCH(CException, e)
{
printf("caught generic exception; rethrowing/n");
THROW_LAST();
printf("this line should never appear/n");
}
END_CATCH
printf("this line should never appear/n");
}
int main()
{
TRY
{
f();
printf("this line should never appear/n");
}
CATCH(CFileException, e)
{
printf("caught file exception/n");
}
AND_CATCH(CMemoryException, e)
{
printf("caught memory exception/n");
}
/* ... handlers for other CException-derived types ... */
AND_CATCH(CException, e)
{
printf("caught generic exception/n");
}
END_CATCH
return 0;
}
/*
When run yields
raising memory exception
caught generic exception; rethrowing
caught memory exception
*/
记住,异常处理函数捕获指向对象的指针,而不是实际的对象。所以,处理函数:
CATCH(CException, e)
{
// ...
}
定义了一个局部指针CException *e指向了被抛出的异常对象。基于C++的多态,这个指针可以引用任何从
CException派生的对象。
如果同一try块有多个处理函数,它们按从上到下的顺序进行匹配搜索的。所以,你应该将处理最派生类的
对象的处理函数放在前面,不然的话,更派生类的处理函数不会接收任何异常的(再次拜多态所赐)。
因为你典型地想捕获CException,MFC定义了几个CException特有宏:
CATCH_ALL(e)和AND_CATCH_ALL(e),等价于CATCH(CException, e)和AND_CATCH(CException, e)。
END_CATCH_ALL ,结束CATCH_ALL... AND_CATCH_ALL队列。
END_TRY等价于CATCH_ALL(e);END_CATCH_ALL。这让TRY... END_TRY中没有处理函数或说是接收所有抛出的异常
。
这个被指的异常对象由MFC隐式析构和归还内存。这一点和标准C++异常处理函数不一样,MFC异常处理不会
让任何人取得被捕获的指针的所有权。因此,你不能用MFC和标准C++体系同时处理相同的异常对象;不然的话
,将导致内存泄漏:引用已被析构的对象,并重复析构和归还同一对象。
1.7 小结
MSDN在线还有另外几篇探索结构化异常处理和MFC异常宏的文章。
下次我将介绍标准C++异常,概述它们的特点及基本原理。我还会将它们和到现在已经看到的方法进行比较。
C++异常处理的基本语法和语义
这次,我来概述标准C++异常处理的基本语法和语义。顺便,我会将它和前两次提到的技术进行比较。(在
本文及以后,我将标准C++异常处理简称为EH,将微软的方法称为SEH。)
1.1 基本语法和语义
EH引入了3个新的C++语言关键字:
catch
throw
try
异常通过如下语句触发
throw [expression]
函数通过“异常规格申明”定义它将抛出什么异常:
throw([type-ID-list])
可选项type-ID-list包含一个或多个类型的名字,以逗号分隔。这些异常靠try块中的异常处理函数进行捕
获。
try compound-statement handler-sequence
处理函数队列包含一个或多个处理函数,形式如下:
catch ( exception-declaration ) compound-statement
处理函数的“异常申明”指明了这个函数将捕获什么类型的异常。
和SEH一样,跟在try和catch后面的语句必须刮在{}内,而整个try块组成一条完整的大语句。
例子:
void f() throw(int, some_class_type)
{
int i;
// ... generate an ´int´ exception
throw i;
// ...
}
int main()
{
try
{
f();
}
catch(int e)
{
// ... handle ´int´ exception ...
}
catch(some_class_type e)
{
// ... handle ´some_class_type´ exception ...
}
// ... possibly other handlers ...
return 0;
}
异常规格申明是EH特有的,SEH和MFC都没有类似的东西。一个空的异常规格申明表明函数不抛出任何异常
:
void f() throw()
{
// ... function throws no exceptions ...
}
如果函数没有异常规格申明,它可以抛出任何类型的异常:
void f()
{
// ... function can throw anything or nothing ...
}
当函数抛异常时,关键字throw通常后面带一个被抛出的对象:
throw i;
然而,throw也可以不带对象:
catch(int e)
{
// ... handle ´int´ exception ...
throw;
}
它的效果是再次抛出当前正被捕获的对象(int e)。因为空throw的作用是再次抛出已存在的异常对象,
所以它必须位于catch语句块中。MFC也有再次抛出异常的功能,SEH则没有,它没有将异常对象交给过处理函数
,所以没什么可再次抛出的。
就象函数原型中的参数申明一样,异常申明也可以是无名的:
catch(char *)
{
// ... handle ´char *´ exception ...
}
当这个处理函数捕获一个char *型的异常对象时,它不能操作这个对象,因为这个对象没有名字。
异常申明还可以是这样的特殊形式:
catch(...)
{
// ... handle any type of exception ...
}
就象不定参数中的“...”一样,异常申明中的“...”可以匹配任何异常的类型。
1.2 标准异常对象的类型
标准库函数可能报告错误。在C标准库中的报错方式在前面说过了。在C++标准库中,有些函数抛出特定的
异常,而另外一些根本不抛任何异常。
因为C++标准中没有明确规定,所以C++的库函数可以抛出任何对象或不抛。但C++标准推荐运行库的实现通
过抛出定义在<stdexecpt>中的异常类型或其派生类型来报告错误:
namespace std
{
class logic_error; // : public exception
class domain_error; // : public logic_error
class invalid_argument; // : public logic_error
class length_error; // : public logic_error
class out_of_range; // : public logic_error
class runtime_error; // : public exception
class range_error; // : public runtime_error
class overflow_error; // : public runtime_error
class underflow_error; // : public runtime_error
}
这些(异常)类只对C++标准库有约束力。在你自己的代码中,你可以抛出(和捕获)任何你所象要的类型
。
1.3 标准中的其它申明
标准库头文件<exception>申明了几个EH类型和函数
namespace std
{
//
// types
//
class bad_exception;
class exception;
typedef void (*terminate_handler)();
typedef void (*unexpected_handler)();
//
// functions
//
terminate_handler set_terminate(terminate_handler) throw();
unexpected_handler set_unexpected(unexpected_handler) throw();
void terminate();
void unexpected();
bool uncaught_exception();
}
提要:
exception是所有标准库抛出的异常的基类。
uncaught_exception()函数在有异常被抛出却没有被捕获时返回true,其它情况返回false。它类似于SEH的函
数AbnormalTermination()。
terminate()是EH的应急处理。它在异常处理体系陷入了不可恢复状态时被调用,经常是因为试图重入(在前一
个异常正处理过程中又抛了一个异常)。
unexpected()在函数抛出一个它没有在“异常规格申明”中申明的异常时被调用。这个预料外的异常可能在退
栈过程中被替换为一个bad_excetion对象。
运行库提供了缺省terminate_handler()和unexpected_handler() 函数处理对应的情况。你可以通过
set_terminate()和set_unexpected()函数替换库的默认版本。
1.4 异常生命期
EH运行于异常生命期的五个阶段:
程序或运行库遇到一个错误状况(阶段1)并且抛出一个异常(阶段2)。
程序的运行停止于异常点,开始搜索异常处理函数。搜索沿调用栈向上搜索(很象SEH终止异常时的行为)。
搜索结束于找到了一个异常申明与异常对象的静态类型相匹配(阶段3)。于是进入相应的异常处理函数。
异常处理函数结束后,跳到此异常处理函数所在的try块下面最近的一条语句开始执行(阶段5)。这个行为意
味着C++标准中异常总是终止。
这些步骤演示于这个简单的例子中:
#include <stdio.h>
static void f(int n)
{
if (n != 0) // Stage 1
throw 123; // Stage 2
}
extern int main()
{
try
{
f(1);
printf("resuming, should never appear/n");
}
catch(int) // Stage 3
{
// Stage 4
printf("caught ´int´ exception/n");
}
catch(char *) // Stage 3
{
// Stage 4
printf("caught ´char *´ exception/n");
}
catch(...) // Stage 3
{
// Stage 4
printf("caught typeless exception/n");
}
// Stage 5
printf("terminating, after ´try´ block/n");
return 0;
}
/*
When run yields
caught ´int´ exception
terminating, after ´try´ block
*/
1.5 基本原理
C标准库的异常体系处理C++语言时有如下难题:
析构函数被忽略。既然C标准库异常体系是为C语言设计的,它们不知道C++的析构函数。尤其,abort()、exit
()和longjmp()在退栈或程序终止时不调用局部对象的析构函数。
繁琐的。查询全局对象或函数返回值导致了代码混乱-你必须在所有可能发生异常的地方进行明确的异常情况
检测,即使是异常情况可能实际上从不发生。因为这种方法是如此繁琐,程序员们可能会故意“忘了”检测异
常情况。
无弹性的。Longjmp()“抛出”的只能是简单的int型。errno和signal()/raise()只使用了很小的一个值域集合
,分辨率很低。Abort()和exit()总是终止程序。Assert()只工作在debug版本中。
非固有的。所有的C标准库异常体系都需要运行库的支持,它不是语言内核支持的。
微软特有的异常处理体系也不是没有限制的:
SEH异常处理函数不是直接捕获一个异常对象,而是通过查询一个(概念性的)类似errno的全局值来判断什么
异常发生了。
SEH异常处理函数不能组合,给定try块的唯有的一个处理函数必须在运行期识别和处理所有的异常事件。
MFC异常处理函数只能捕获CException及派生类型的指针。
通过包含定义了MFC异常处理函数的宏的头文件,程序包含了数百个无关的宏和申明。
MFC和SEH都是专属于与Microsoft兼容的开发环境和Windows运行平台的。
标准C++异常处理避免了这些短处:
析构安全。在抛异常而进行退栈时,局部对象的析构函数被按正确的顺序调用。
不引人注目的。异常的捕获是暗地里的和自动的。程序员无需因错误检测而搞乱设计。
精确的。因为几乎任何对象都可以被抛出和捕获,程序员可以控制异常的内容和含义。
可伸缩的。每个函数可以有多个try块。每个try块可以有单个或一组处理函数。每个处理函数可以捕获单个类
型,一组类型或所有类型的异常。
可预测的。函数可以指定它们将抛的异常类型,异常处理函数可以指定它们捕获什么类型的异常。如果程序违
反了其申明,标准库将按可预测的、用户定义的方式运行。
固有的。EH是C++语言的一部分。你可以定义、throw和catch异常而不需要包含任何库。
标准的。EH在所有的标准C++的实现中都可用。
基于更完备的想法,C++标准委员会考虑过两个EH的设计,在D&E的16章。(For a more complete
rationale, including alternative EH designs considered by the C++ Standard´s committee, check out
Chapter 16 of the D&E.)
1.6 小结
下次,我将更深入挖掘EH的语言核心特性和EH的标准库支持。我也将展示Microsoft Visual C++实现EH的
内幕。我将开始标志出EH的那些Visual C++只部分支持或完全不支持的特性,并且寻找绕过这些限制的方法。
在我相信设计EH的基本原理是健全的的同时,我也认为EH无意中包含了一些严重的后果。不用责备C++标准的制
订者的短视,我理解设计和实现有效的异常处理是多么的难。当我们遭遇到这些无意中的后果时,我将展示它
们对你代码的微妙影响,并且推荐一些技巧来减轻其影响。