[转]C++异常处理 13
• ExceptionFlags包含有关异常的标志。当前只有两个值,分别是0(指出一个可以继续的异常)和E X C E P T I O N _ N O N C O N T I N U A B L E(指出一个不可继续的异常)。在一个不可继续的异常之后,若要继续执行,会引发一个E X C E P T I O N _ N O N C O N T I N U A B L E _E X C E P T I O N异常。
• E x c e p t i o n R e c o r d指向另一个未处理异常的E X C E P T I O N _ R E C O R D结构。在处理一个异常的时候,有可能引发另外一个异常。例如,异常过滤器中的代码就可能用零来除一个数。当嵌套异常发生时,可将异常记录链接起来,以提供另外的信息。如果在处理一个异常过滤器的过程当中又产生一个异常,就发生了嵌套异常。如果没有未处理异常,这个成员就包含一个N U L L。
• ExceptionAddress指出产生异常的C P U指令的地址。
• N u m b e r P a r a m e t e r s 规定了与异常相联系的参数数量( 0 到1 5 )。这是在E x c e p t i o n I n f o r m a t i o n数组中定义的元素数量。对几乎所有的异常来说,这个值都是零。
• E x c e p t i o n I n f o r m a t i o n规定一个附加参数的数组,用来描述异常。对大多数异常来说,数组元素是未定义的。
E X C E P T I O N _ R E C O R D结构的最后两个成员,N u m b e r P a r a m e t e r s和E x c e p t i o n I n f o r m a t i o n向异常过滤器提供一些有关异常的附加信息。目前只有一种类型的异常提供附加信息,就是E X C E P T I O N _ A C C E S S _ V I O L AT I O N。所有其他可能的异常都将N u m b e r P a r a m e t e r s设置成零。我们可以检验E x c e p t i o n I n f o r m a t i o n的数组成员来查看关于所产生异常的附加信息。对于一个E X C E P T I O N _ A C C E S S _ V I O L AT I O N异常来说,E x c e p t i o n I n f o r m a t i o n [ 0 ]包含一个标志,指出引发这个存取异常的操作的类型。如果这个值是0,表示线程试图要读不可访问的数据。如果这个值是1,表示线程要写不可访问的数据。ExceptionInformation[1] 指出不可访问数据的地址。通过使用这些成员,我们可以构造异常过滤器,提供大量有关程序的信息。
本质上,对C P U上每一个可用的寄存器,这个结构相应地包含一个成员。当一个异常被引发时,可以通过检验这个结构的成员找到更多的信息。遗憾的是,为了得到这种可能的好处,要求程序员编写依赖于平台的代码,以确认程序所运行的机器,使用适当的CONTEXT结构。最好的办法是在代码中安排一个# ifdefs指令。Windows支持的不同CPU的CONTEXT结构定义在WinNT.h文件中。
软件异常
到目前为止,我们一直在讨论硬件异常,也就是C P U捕获一个事件并引发一个异常。在代码中也可以强制引发一个异常。这也是一个函数向它的调用者报告失败的一种方法。传统上,失败的函数要返回一些特殊的值来指出失败。函数的调用者应该检查这些特殊值并采取一种替代的动作。通常,这个调用者要清除所做的事情并将它自己的失败代码返回给它的调用者。这种错误代码的逐层传递会使源程序的代码变得非常难于编写和维护。另外一种方法是让函数在失败时引发异常。用这种方法,代码更容易编写和维护,而且也执行得更好,因为通常不需要执行那些错误测试代码。实际上,仅当发生失败时也就是发生异常时才执行错误测试代码。但令人遗憾的是,许多开发人员不习惯于在错误处理中使用异常。这有两方面的原因。第一个原因是多数开发人员不熟悉S E H。即使有一个程序员熟悉它,但其他程序员可能不熟悉它。如果一个程序员编写了一个引发异常的函数,但其他程序员并不编写S E H框架来捕获这个异常,那么进程就会被操作系统结束。开发人员不使用S E H的第二个原因是它不能移植到其他操作系统。许多公司的产品要面向多种操作系统,因此希望有单一的源代码作为产品的基础,这是可以理解的。S E H是专门针对Wi n d o w s的技术。
本段讨论通过异常返回错误有关的内容。首先,让我们看一看Windows Heap函数,例如HeapCreate、HeapAlloc等。通常当某个堆( h e a p)函数失败,它会返回N U L L来指出失败。然而可以对这些堆函数传递HEAP_GENERATE_EXCEPTIONS标志。如果使用这个标志并且函数失败,函数不会返回N U L L,而是由函数引发一个STATUS_NO_MEMOR软件异常,程序代码的其他部分可以用S E H框架来捕获这个异常。
如果想利用这个异常,可以编写你的t r y块,好像内存分配总能成功。如果内存分配失败,可以利用e x c e p t块来处理这个异常,或通过匹配t r y块与f i n a l l y块,清除函数所做的事。这非常方便。
程序捕获软件异常采取的方法与捕获硬件异常完全相同。也就是说,前面介绍的内容可以同样适用于软件异常。这里讨论如何让你自己的函数引发软件异常,作为指出失败的方法。实际上,可以用类似于微软实现堆函数的方法来实现你的函数:让函数的调用者传递一个标志,告诉函数如何指出失败。
引发一个软件异常很容易,只需要调用RaiseException函数:
RaiseException
This function raises an exception in the calling thread.
void RaiseException(
DWORD dwExceptionCode,
DWORD dwExceptionFlags,
DWORD nNumberOfArguments,
const DWORD *lpArguments);
第一个参数d w E x c e p t i o n C o d e是标识所引发异常的值。H e a p A l l o c函数对这个参数设定S TAT U S _ N O _ M E M O RY。如果程序员要定义自己的异常标识符,应该遵循标准Wi n d o w s错误代码的格式,像Wi n E r r o r. h文件中定义的那样。如果要建立你自己的异常代码,要填充D W O R D的4个部分:
• 第3 1位和第3 0位包含严重性系数( s e v e r i t y )。
• 第2 9位是1(0表示微软建立的异常,如H e a p A l l o c的S TAT U S _ N O _ M E M O RY)。
• 第2 8位是0。
• 第2 7位到1 6位是某个微软定义的设备代码。
• 第1 5到0位是一个任意值,用来标识引起异常的程序段。
R a i s e E x c e p t i o n 的第二个参数d w E x c e p t i o n F l a g s ,必须是0 或E X C E P T I O N _N O N C O N T I N U A B L E。
本质上,这个标志是用来规定异常过滤器返回E X C E P T I O N _CONTINUE _EXECUTION来响应所引发的异常是否合法。如果没有向R a i s e E x c e p t i o n传递EXCEPTION_ NONCONTINUABLE参数值,则过滤器可以返回E X C E P T I O N _ C O N T I N U E _E X E C U T I O N。正常情况下,这将导致线程重新执行引发软件异常的同一C P U指令。但微软已做了一些动作,所以在调用R a i s e E x c e p t i o n函数之后,执行会继续进行。如果你向R a i s e E x c e p t i o n传递了E X C E P T I O N _ N O N C O N T I N U A B L E标志,你就是在告诉系统,你引发异常的类型是不能被继续执行的。这个标志在操作系统内部被用来传达致命(不可恢复)的错误信息。另外,当H e a p A l l o c引发S TAT U S _ N O _ M E M O RY软件异常时,它使用E X C E P T I O N _ N O N C O N T I N U A B L E标志来告诉系统,这个异常不能被继续。意思就是没有办法强制分配内存并继续运行。如果一个过滤器忽略E X C E P T I O N _ N O N C O N T I N U A B L E并返回E X C E P T I O N _ C O N T I N U E _
E X E C U T I O N,系统会引发新的异常:E X C E P T I O N _ N O N C O N T I N U A B L E _ E X C E P T I O N。
当程序在处理一个异常的时候,有可能又引发另一个异常。比如说,一个无效的内存存取