http://www.51cto.com/art/200511/11258.htm
http://localvar.blogchina.com/blog/1068938.htmlVishal Kochhar
C++编译器如何实现异常处理- -
从实现的角度讲解VC++中的异常处理机制。本文在网上已经有了几个译本,但都不完整,所以我决定自己把它翻译过来。翻译过程中,力求信雅达,不足之处,敬请指出!
C++编译器如何实现异常处理
作者:Vishal Kochhar 查看原文 翻译: 局部变量 email:localvar#sohu.com与传统语言相比,C++的一项革命性创新就是它支持异常处理。传统的错误处理方式经常满足不了要求,而异常处理则是一个极好的替代解决方案。它将正常代码 和错误处理代码清晰的划分开来,程序变得非常干净并且容易维护。本文讨论了编译器如何实现异常处理。我将假定你已经熟悉异常处理的语法和机制。本文还提供 了一个用于VC++的异常处理库,要用库中的处理程序替换掉VC++提供的那个,你只需要调用下面这个函数:
install_my_handler(); |
与其它C++特性一样,C++标准并没有规定编译器应该如何来实现异常处理。这意味着每一个编译器的提供商都可以用它们认为恰当的方式来实现它。下面我会 描述一下VC++是怎么做的,但即使你使用其它的编译器或操作系统①,本文也应该会是一篇很好的学习材料。VC++的实现方式是以windows系统的结 构化异常处理(SEH)②为基础的。
结构化异常处理—概述
在本文的讨论中,我认为异常或者是被明确的抛出的,或者是由于除零溢出、空指针访问等引起的。当它发生时会产生一个中断,接下来控制权就会传递到操作系统 的手中。操作系统将调用异常处理程序,检查从异常发生位置开始的函数调用序列,进行堆栈展开和控制权转移。Windows定义了结构 "EXCEPTION_REGISTRATION",使我们能够向操作系统注册自己的异常处理程序。
struct EXCEPTION_REGISTRATION { EXCEPTION_REGISTRATION* prev; DWORD handler; }; |
mov FS:[0], exc_regp |
那么,那个异常回调函数长什么样呢?在excpt.h中,windows定义了它的原形:
EXCEPTION_DISPOSITION (*handler)( _EXCEPTION_RECORD *ExcRecord, void* EstablisherFrame, _CONTEXT *ContextRecord, void* DispatcherContext); |
#include #include using std::cout; using std::endl; struct EXCEPTION_REGISTRATION { EXCEPTION_REGISTRATION* prev; DWORD handler; }; EXCEPTION_DISPOSITION myHandler( _EXCEPTION_RECORD *ExcRecord, void * EstablisherFrame, _CONTEXT *ContextRecord, void * DispatcherContext) { cout << "In the exception handler" << endl; cout << "Just a demo. exiting..." << endl; exit(0); return ExceptionContinueExecution; //不会运行到这 } int g_div = 0; void bar() { //初始化一个EXCEPTION_REGISTRATION结构 EXCEPTION_REGISTRATION reg, *preg = ® reg.handler = (DWORD)myHandler; //取得当前异常处理链的"头" DWORD prev; _asm { mov EAX, FS:[0] mov prev, EAX } reg.prev = (EXCEPTION_REGISTRATION*) prev; //注册! _asm { mov EAX, preg mov FS:[0], EAX } //产生一个异常 int j = 10 / g_div; //异常,除零溢出 } int main() { bar(); return 0; } /*-------输出------------------- In the exception handler Just a demo. exiting... ---------------------------------*/ |
函数和堆栈
堆栈是用来保存局部对象的连续内存区。更明确的说,每个函数都有一个相关的栈桢(stack frame)来保存它所有的局部对象和表达式计算过程中用到的临时对象,至少理论上是这样的。但现实中,编译器经常会把一些对象放到寄存器中以便能以更快 的速度访问。堆栈是一个处理器(CPU)层次的概念,为了操纵它,处理器提供了一些专用的寄存器和指令。
图1是一个典型的堆栈,它示出了函数foo调用bar,bar又调用widget时的情景。请注意堆栈是向下增长的,这意味着新压入的项的地址低于原有项的地址。
通常编译器使用EBP寄存器来指示当前活动的栈桢。本例中,CPU正在运行widget,所以图中的EBP指向了widget的栈桢。编译器在编译时将所 有局部对象解析成相对于栈桢指针(EBP)的固定偏移,函数则通过栈桢指针来间接访问局部对象。举个例子,典型的,widget访问它的局部变量时就是通 过访问栈桢指针以下的、有着确定位置的几个字节来实现的,比如说EBP-24。
上图中也画出了ESP寄存器,它叫栈指针,指向栈的最后一项。在本例中,ESP指着widget的栈桢的末尾,这也是下一个栈桢(如果它被创建的话)的开始位置。
处理器支持两种类型的栈操作:压栈(push)和弹栈(pop)。比如,
pop EAX |
push EBP |
编译器编译一个函数时,会在它的开头添加一些代码来为其创建并初始化栈桢,这些代码被称为序言(prologue);同样,它也会在函数的结尾处放上代码来清除栈桢,这些代码叫做尾声(epilogue)。
一般情况下,序言是这样的:
Push EBP ; 把原来的栈桢指针保存到栈上 Mov EBP, ESP ; 激活新的栈桢 Sub ESP, 10 ; 减去一个数字,让ESP指向栈桢的末尾 |
尾声所做的正好和序言相反,它必须把当前栈桢从栈上清除掉:
Mov ESP, EBP Pop EBP ; 激活主调函数的栈桢 Ret ; 返回主调函数 |
一旦CPU遇到返回指令,它就要做以下两件事:把返回地址从栈中弹出,然后跳转到那个地址去。返回地址是主调函数执行call指令调用被调函数时自动压栈 的。Call指令执行时,会先把紧随在它后面的那条指令的地址(被调函数的返回地址)压入栈中,然后跳转到被调函数的开始位置。图2更详细的描绘了运行时 的堆栈。如图所示,主调函数把被调函数的参数也压进了堆栈,所以参数也是栈桢的一部分。函数返回后,主调函数需要移除这些参数,它通过把所有参数的总体积 加到ESP上来达到目的,而这个体积可以在编译时知道:
Add ESP, args_size |
Ret 24 |
![](https://i-blog.csdnimg.cn/blog_migrate/ddabb834bbda3129d67c3063fcbe6c6c.gif)
C++和异常
回忆一下我在第一节中介绍的EXCEPTION_REGISTRATION结构,我们曾用它向操作系统注册了发生异常时要被调用的回调函数。VC++也是这么做的,不过它扩展了这个结构的语义,在它的后面添加了两个新字段:struct EXCEPTION_REGISTRATION { EXCEPTION_REGISTRATION* prev; DWORD handler; int id; DWORD ebp; }; |
VC++编译函数时会为它生成两部分数据:
a)异常回调函数
b)一个包含函数重要信息的数据结构,这些信息包括catch块、这些块的地址和这些块所关心的异常的类型等等。我把这个结构称为funcinfo,有关它的详细讨论也在下一节。
图3是考虑了异常处理之后的运行时堆栈。widget的异常回调函数位于由FS:[0]指向的异常处理链的开始位置(这是由widget的序言设置的)。 异常处理程序把widget的funcinfo结构的地址交给函数__CxxFrameHandler,__CxxFrameHandler会检查这个结 构看函数中有没有catch块对当前的异常感兴趣。如果没有的话,它就返回ExceptionContinueSearch给操作系统,于是操作系统会从 异常处理链表中取得下一个结点,并调用它的异常处理程序(也就是调用当前函数的那个函数的异常处理程序)。
![](https://i-blog.csdnimg.cn/blog_migrate/c00ecc0991407a70afb527e07ac31747.gif)
这一过程将一直进行下去——直到处理程序找到一个能处理当前异常的catch块为止,这时它就不再返回操作系统了。但是在调用catch块之前(由于有 funcinfo结构,所以知道catch块的入口,参见图3),必须进行堆栈展开,也就是清理掉当前函数的栈桢下面的所有其他的栈桢。这个操作稍微有点 复杂,因为:异常处理程序必须找到异常发生时生存在这些栈桢上的所有局部对象,并依次调用它们的析构函数。后面我将对此进行详细介绍。
异常处理程序把这项工作委托给了各个栈桢自己的异常处理程序。从FS:[0]指向的异常处理链的第一个结点开始,它依次调用每个结点的处理程序,告诉它堆 栈正在展开。与之相呼应,这些处理程序会调用每个局部对象的析构函数,然后返回。此过程一直进行到与异常处理程序自身相对应的那个结点为止。
由于catch块是函数的一部分,所以它使用的也是函数的栈桢。因此,在调用catch块之前,异常处理程序必须激活它所隶属的函数的栈桢。
其次,每个catch块都只接受一个参数,其类型是它希望捕获的异常的类型。异常处理程序必须把异常对象本身或者是异常对象的引用拷贝到catch块的栈 桢上,编译器在funcinfo中记录了相关信息,处理程序根据这些信息就能知道到哪去拷贝异常对象了。
拷贝完异常并激活栈桢 后,处理程序将调用catch块。而catch块将把控制权下一步要转移到的地址返回来。请注意:虽然这时堆栈已经展开,栈桢也都被清除了,但它们占据的 内存空间并没有被覆盖,所有的数据都还好好的待在栈上。这是因为异常处理程序仍在执行,象其他函数一样,它也需要栈来存放自己的局部对象,而其栈桢就位于 发生异常的那个函数的栈桢的下面。catch块返回以后,异常处理程序需要"杀掉"异常对象。此后,它让ESP指向目标函数(控制权要转移到的那个函数) 的栈桢的末尾——这样就把(包括它自己的在内的)所有栈桢都删除了,然后再跳转到catch块返回的那个地址去,就胜利的完成整个异常处理任务了。但它怎 么知道目标函数的栈桢末尾在哪呢?事实上它没法知道,所以编译器把这个地址保存到了栈桢上(由前言来完成),如图3所示,栈桢指针EBP下面第16个字节 就是。
当然,catch块也可能抛出新异常,或者是将原来的异常重新抛出。处理程序必须对此有所准备。如果是抛出新异常,它必须杀掉原来的那个;而如果是重新抛出原来的异常,它必须能继续传播(propagate)这个异常。
这里我要特别强调一点:由于每个线程有自己独立的堆栈,所以每个线程也都有自己独立的、由FS:[0]指向的EXCEPTION_REGISTRATION链。
C++和异常—2
图4是funcinfo的布局,注意这里的字段名可能与VC++编译器实际使用的不完全一致,而且我也只给出了和我们的讨论相关的字段。堆栈展开表(unwind table)的结构留到下节再讨论。![](https://i-blog.csdnimg.cn/blog_migrate/d840b2510bb9cfe620a62d0306360a0a.gif)
异常处理程序在函数中查找catch块时,它首先要判断异常发生的位置是否在当前函数(发生异常的那个函数)的一个try块中。是则查找与此try块相关的catch块表,否则直接返回。
先来看看它怎样找try块。编译时,编译器给每个try块都分配了start id和end id。通过funcinfo结构,异常处理程序可以访问这两个id,见图4。编译器为函数中的每个try块都生成了相关的数据结构。
上一节中,我说过VC++给EXCEPTION_REGISTRATION结构加上了一个id字段。回忆一下图3,这个结构位于函数的栈桢上。异常发生 时,处理程序读出这个值,看它是否在try块的两个id确定的区间[start id,end id]中。是的话,异常就发生在这个try块中;否则继续查看try块表中的下一个try块。
谁负责更新id的值,它的值又应该是什么呢?原来,编译器会在函数的多个位置安插代码来更新id的值,以反应程序的实时运行状态。比如说,编译器会在进入try块的地方加上一条语句,把try块的start id写到栈桢上。
找到try块后,处理程序就遍历与其关联的catch块表,看是否有对当前异常感兴趣的catch块。在try块发生嵌套时,异常将既源于内层try块, 也源于外层try块。这种情况下,处理程序应该按先内后外的顺序查找catch块。但它其实没必要关心这些,因为,在try块表中,VC++总是把内层 try块放在外层try块的前面。
异常处理程序还有一个难题就是"如何根据catch块的相关数据结构判断这个catch块是否愿意处理当前异常"。这是通过比较异常的类型和catch块的参数的类型来完成的。例如下面这个程序:
void foo() { try { throw E(); } catch(H) { //. } } |
catch块的参数类型可以通过funcinfo结构得到,但异常的type_info从哪来呢?当编译器碰到
throw E(); |
![](https://i-blog.csdnimg.cn/blog_migrate/2debcd9da5a25af5222990dab4f81bd6.gif)
在catch块的参数是基类,而异常是派生类时,异常处理程序也应该调用catch块。然而,这种情况下,比较它们的type_info绝对是不相等,因 为它们本来就不是相同的类型。而且,type_info类也没有提供任何其他函数或运算符来指出一个类是另一个类的基类。但异常处理程序还必须得去调用 catch块!为了解决这个问题,编译器只能为处理程序提供更多的信息:如果异常是派生类,那么etypeinfo_table(通过 excpt_info访问)将包含多个指向etype_info(扩展了type_info,这个名字是我启的)的指针,它们分别指向了各个基类的 etype_info。这样,处理程序就可以把catch块的参数和所有这些type_info比较,只要有一个相同,就调用catch块。
在结束这一部分之前,还有最后一个问题:异常处理程序是怎么知道异常和excpt_info结构的?下面我就要回答这个问题。
VC++会把throw语句翻译成下面的样子:
//throw E(); //编译器会为E生成excpt_info结构 E e = E(); //在栈上创建异常 _CxxThrowException(&e, E_EXCPT_INFO_ADDR); |
EXCEPTION_DISPOSITION (*handler)( _EXCEPTION_RECORD* ExcRecord, void* EstablisherFrame, _CONTEXT *ContextRecord, void* DispatcherContext); |
struct _EXCEPTION_RECORD { DWORD ExceptionCode; DWORD ExceptionFlags; _EXCEPTION_RECORD* ExcRecord; PVOID ExceptionAddress; DWORD NumberParameters; DWORD ExceptionInformation[15]; }EXCEPTION_RECORD; |
ExceptionFlags字段用于告诉异常处理程序应该采取什么操作。如果它是 EH_UNWINDING(见Except.inc),那是说堆栈正在展开,这时,处理程序要清理栈桢,然后返回。否则处理程序应该在函数中查找 catch块并调用它。清理栈桢意味着必须找到异常发生时生存在栈桢上的所有局部对象,并调用其析构函数,下一节我们将就此进行详细讨论。
清理栈桢
C++标准明确指出:堆栈展开工作必须调用异常发生时所有生存的局部对象的析构函数。如下面的代码:int g_i = 0; void foo() { T o1, o2; { T o3; } 10/g_i; //这里会发生异常 T o4; //... } |
前面已经说过,编译器会在函数的很多地方安插代码来记录当前的运行状态。实际上,编译器在函数中设置了一些关键区域,并为它们分配了id,进入关键区域时 要记录它的id,退出时恢复前一个id。try块就是一个例子,其id就是start id。所以,在try块的入口,编译器会把它的start id记到栈桢上去。局部对象从创建到销毁也确定了一个关键区域,或者,换句话说,编译器给每个局部对象分配了唯一的id,例如下面的程序:
void foo() { T t1; //. } |
void foo() { T t1; _id = t1_id; //编译器插入的语句 //. } |
清理栈桢时,异常处理程序读出id的值(通过EXCEPTION_REGISTRATION结构的id字段或栈桢指针EBP下面的4个字节来访问)。这个 id可以表明,函数在运行到与它相关联的那个点之前没有发生异常。所有在这一点之前定义的对象都已初始化,应该调用这些对象中的一部分或全部对象的析构函 数。请注意某些对象是属于子块(如前面代码中的o3)的,发生异常时可能已经销毁了,不应该调用它们的析构函数。
编译器还为函数生成了另一个数据结构——堆栈展开表(unwindtable,我启的名字),它是一个unwind结构的数组,可通过funcinfo来 访问,如图4所示。函数的每个关键区域都有一个unwind结构,这些结构在展开表中出现的次序和它们所对应的区域在函数中的出现次序完全相同。一般 unwind结构也会关联一个对象(别忘了,每个对象的定义都开辟了关键区域,并有id与其对应),它里面有如何销毁这个对象的信息。每当编译器碰到对象 定义,它就生成一小段代码,这段代码知道对象在栈桢上的地址(就是它相对于栈桢指针的偏移),并能销毁它。unwind结构中有一个字段用于保存这段代码 的入口地址:
typedef void (*CLEANUP_FUNC)(); struct unwind { int prev; CLEANUP_FUNC cf; }; |
![](https://i-blog.csdnimg.cn/blog_migrate/499ee63857d1f5fc52a0ea1a813003e9.gif)
现在把new运算符也加进来,对于下面的代码:
T* p = new T(); |
更进一步说,构造函数抛出异常时,对象可能刚刚构造了一部分,如果它有子成员对象或子基类对象,并且发生异常时它们中的一部分已经构造完成的话,就必须调用这些对象的析构函数。和普通函数一样,编译器也给构造函数生成了相关的数据来帮助完成这个任务。
展开堆栈时,异常处理程序调用的是用户定义的析构函数,这一点你必须注意,因为它也有可能抛出异常!C++标准规定堆栈展开过程中,析构函数不能抛出异常,否则系统将调用std::terminate。
实现
本节我们讨论其他三个有待详细解释的问题:a) 如何安装异常处理程序
b) catch块重新抛出异常或抛出新异常时应该如何处理
c) 如何对所有线程提供异常处理支持
随同本文,有一个演示项目,查看其中的readme.txt文件可以得到一些编译方面的帮助①。
第一项任务是安装异常处理程序,也就是把VC++的处理程序替换掉。从前面的讨论中,我们已经清楚地知道__CxxFrameHandler函数是VC+ +所有异常处理工作的入口。编译器为每个函数都生成一段代码,它们在发生异常时被调用,把相应的funcinfo结构的指针交给 __CxxFrameHandler。
install_my_handler()函数会改写 __CxxFrameHandler的入口处的代码,让程序跳转到my_exc_handler()函数。不过,__CxxFrameHandler位于 只读的内存页,对它的任何写操作都会导致访问违例,所以必须首先用VirtualProtectEx把该内存页的保护方式改成可读写,等改写完毕后,再改 回只读。写入的数据是一个jmp_instr结构。
//install_my_handler.cpp #include #include "install_my_handler.h" //C++默认的异常处理程序 extern "C" EXCEPTION_DISPOSITION __CxxFrameHandler( struct _EXCEPTION_RECORD* ExceptionRecord, void* EstablisherFrame, struct _CONTEXT* ContextRecord, void* DispatcherContext ); namespace { char cpp_handler_instructions[5]; bool saved_handler_instructions = false; } namespace my_handler { //我的异常处理程序 EXCEPTION_DISPOSITION my_exc_handler( struct _EXCEPTION_RECORD *ExceptionRecord, void * EstablisherFrame, struct _CONTEXT *ContextRecord, void * DispatcherContext ) throw(); #pragma pack(push, 1) struct jmp_instr { unsigned char jmp; DWORD offset; }; #pragma pack(pop) bool WriteMemory(void* loc, void* buffer, int size) { HANDLE hProcess = GetCurrentProcess(); //把包含内存范围[loc,loc+size]的页面的保护方式改成可读写 DWORD old_protection; BOOL ret = VirtualProtectEx(hProcess, loc, size, PAGE_READWRITE, &old_protection); if(ret == FALSE) return false; ret = WriteProcessMemory(hProcess, loc, buffer, size, NULL); //恢复原来的保护方式 DWORD o2; VirtualProtectEx(hProcess, loc, size, old_protection, &o2); return (ret == TRUE); } bool ReadMemory(void* loc, void* buffer, DWORD size) { HANDLE hProcess = GetCurrentProcess(); DWORD bytes_read = 0; BOOL ret = ReadProcessMemory(hProcess, loc, buffer, size, &bytes_read); return (ret == TRUE && bytes_read == size); } bool install_my_handler() { void* my_hdlr = my_exc_handler; void* cpp_hdlr = __CxxFrameHandler; jmp_instr jmp_my_hdlr; jmp_my_hdlr.jmp = 0xE9; //从__CxxFrameHandler+5开始计算偏移,因为jmp指令长5字节 jmp_my_hdlr.offset = reinterpret_cast<char*>(my_hdlr) - (reinterpret_cast<char*>(cpp_hdlr) + 5); if(!saved_handler_instructions) { if(!ReadMemory(cpp_hdlr, cpp_handler_instructions, sizeof(cpp_handler_instructions))) return false; saved_handler_instructions = true; } return WriteMemory(cpp_hdlr, &jmp_my_hdlr, sizeof(jmp_my_hdlr)); } bool restore_cpp_handler() { if(!saved_handler_instructions) return false; else { void* loc = __CxxFrameHandler; return WriteMemory(loc, cpp_handler_instructions, sizeof(cpp_handler_instructions)); } } } |
现在重新回到异常处理这个主题上来。调用catch块时,它可能重新抛出异常或抛出新异常。前一种情况下,异常处理程序必须继续传播 (propagate)当前异常;后一种情况下,它需要在继续之前销毁原来的异常。此时,处理程序要面对两个难题:"如何知道异常是源于catch块还是 程序的其他部分"和"如何跟踪原来的异常"。我的解决方法是:在调用catch块之前,把当前异常保存在exception_storage对象中,并注 册一个专用于catch块的异常处理程序——catch_block_protector。调用get_exception_storage()函数,就 能得到exception_storage对象:
exception_storage* p = get_exception_storage(); p->set(pexc, pexc_info); 注册 catch_block_protector; 调用catch块; //.... |
//------------------------------------------------------------------- // 如果这个处理程序被调用了,可以断定是catch块(重新)抛出了异常。 // 异常处理程序(my_handler)在调用catch块之前注册了它。其任务是判断 // catch块抛出了新异常还是重新抛出了原来的异常,并采取相应的操作。 // 在前一种情况下,它需要销毁传递给catch块的前一个异常对象;在后一种 // 情况下,它必须找到原来的异常并将其保存到ExceptionRecord中供异常 // 处理程序使用。 //------------------------------------------------------------------- EXCEPTION_DISPOSITION catch_block_protector( _EXCEPTION_RECORD* ExceptionRecord, void* EstablisherFrame, struct _CONTEXT *ContextRecord, void* DispatcherContext ) throw () { EXCEPTION_REGISTRATION *pFrame; pFrame= reinterpret_cast (EstablisherFrame); if(!(ExceptionRecord->ExceptionFlags & (_EXCEPTION_UNWINDING | _EXCEPTION_EXIT_UNWIND))) { void *pcur_exc = 0, *pprev_exc = 0; const excpt_info *pexc_info = 0, *pprev_excinfo = 0; exception_storage* p = get_exception_storage(); pprev_exc = p->get_exception(); pprev_excinfo= p->get_exception_info(); p->set(0, 0); bool cpp_exc = ExceptionRecord->ExceptionCode = = MS_CPP_EXC; get_exception(ExceptionRecord, &pcur_exc); get_excpt_info(ExceptionRecord, &pexc_info); if(cpp_exc && 0 == pcur_exc && 0 == pexc_info) //重新抛出 { ExceptionRecord->ExceptionInformation[1] = reinterpret_cast (pprev_exc); ExceptionRecord->ExceptionInformation[2]= reinterpret_cast (pprev_excinfo); } else { exception_helper::destroy(pprev_exc, pprev_excinfo); } } return ExceptionContinueSearch; } |
exception_storage* get_exception_storage() { static exception_storage es; return &es; } |
Excptstorage.cpp中给出了get_exception_storage()函数的实现。它会被编译成动态链接库,因为我们可以籍此知道线 程的创建和退出——系统在这两种情况下都会调用所有(当前进程加载的)dll的DllMain()函数,这让我们有机会创建特定于线程的数据,也就是 exception_storage对象。
//excptstorage.cpp #include "excptstorage.h" #include namespace { DWORD dwstorage; } namespace my_handler { __declspec(dllexport) exception_storage* get_exception_storage() throw () { void * p = TlsGetValue(dwstorage); return reinterpret_cast (p); } } BOOL APIENTRY DllMain( HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ) { using my_handler::exception_storage; exception_storage *p; switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: //主线程(第一个线程)不会收到DLL_THREAD_ATTACH通知,所以, //与其相关的操作也放在这了 dwstorage = TlsAlloc(); if (-1 == dwstorage) return FALSE; p = new exception_storage(); TlsSetValue(dwstorage, p); break ; case DLL_THREAD_ATTACH: p = new exception_storage(); TlsSetValue(dwstorage, p); break; case DLL_THREAD_DETACH: p = my_handler::get_exception_storage(); delete p; break ; case DLL_PROCESS_DETACH: p = my_handler::get_exception_storage(); delete p; break ; } return TRUE; } |
结论
综上所述,异常处理是在操作系统的协助下,由C++编译器和运行时异常处理库共同完成的。注释和参考资料
① 本文写作期间,微软发布了Visual Studio 7.0。本文的异常处理库主要是在运行于奔腾处理器的windows2000上使用VC++6.0编译和测试的。但我也在VC++5.0和VC++7.0 beta版上测试过。6.0和7.0之间有一些差别,6.0先把异常(或其引用)拷贝到catch块的栈桢上,然后在调用catch块之前进行堆栈展开; 7.0则先进行堆栈展开。在这方面,我的库代码的行为比较接近6.0版。② 参见Matt Pietrek发表在MSDN上的文章《structured exception handling》。
③ 如果一个函数既不含try块,也没有定义任何具有"有为的析构函数"的对象,那么编译器将不为它生成用于异常处理的数据。