jimmy 战志杰 编译
本文编译自Jeffrey Richter先生的“Advanced Windows”部分章节。 1、引言 在“C++中例外的处理”一文中(见计算机世界网2001年12月20日),我们讨论了C++中的例外(或异常)处理。本文将进一步探讨Visual C++中的结构异常处理。 想象一下,如果在编程过程中你不需要考虑任何错误,你的程序永远不会出错,有足够的内存,你需要的文件永远存在,这将是一件多么愉快的事。这时你的程序不需要太多的if语句转来转去,非常容易写,容易读,也容易理解。如果你认为这样的编程环境是一种梦想,那么你就会喜欢结构异常处理(structu reed exception handling)。 结构异常处理的本质就是让你专心于如何去完成你的任务。如果在程序运行过程中出现任何错误,系统会接收(catch)并通知(notify)你。虽然利用结构异常处理你不可能完全忽略你的程序出错的可能性,但是结构异常处理确确实实允许你将你的主要任务与错误处理分离开来。这种分离使得你可以集中精力于你的工作,而在以后在考虑可能的错误。 结构异常处理的主要工作是由编译器来完成的,而不是由操作系统。编译器在遇到例外程序段时需要产生额外的特殊代码来支持结构异常处理。所以,每一个编译器产品供应商可能使用自己的语法和规定。这里我们采用微软的Visual C++编译器来进行讨论。 注意不要将这里讨论的结构异常处理与C++中的异常处理混为一谈。C++中的异常处理是另一种形式的异常处理,它使用了C++的关键词catch和throw。 微软最早在Visual C++版本2.0引进结构异常处理。结构异常处理主要由两部分组成:中断处理(termination handling)和例外处理(exception handling)。 2、中断处理句柄(termination handler) 2.1、中断处理句柄定义 中断处理句柄保证了,不论进程如何离开另一程序段--这里称之为守卫体(guarded body),该句柄内的程序段永远会被调用和执行。微软的Visual C++编译器的中断处理句柄语法为 __try { // Guarded body . . . } __finally { // Termination handler . . . } 这里的__try和__finally勾画出了中断处理句柄的两个部分。在上面的例子中,操作系统和编译器一起保证了不论包含在__try内的程序段出现何种情况,包含在__finally内的程序段永远会被运行。不论你在__try内的程序段中调用return、goto或longjump,__finally内的中断处理句柄永远会被调用。其流程为 // 1、执行try程序段前的代码 __try { // 2、执行try程序段内的代码 } __finally { // 3、执行finally程序段内的代码 } // 4、执行finally程序段后的代码 2.2、几个例子 下面我们通过几个具体例子来讨论中断处理句柄是如何工作的。 2.2.1、例1--Funcenstein1 清单一给出了我们的第一个例子。 DWORD Funcenstein1(void) { DWORD dwTemp; // 1. Do any processing here. . . . __try { // 2. request permission to access protected data, and then use it. WaitForSingleObject(g_hSem, INFINITE); g_dwProtectedData = 5; dwTemp = g_dwProtectedData; } __finally { // 3. Allow others to use protected data. ReleaseSemaphore(g_hSem, 1, NULL); } // 4. Continue processing. return (dwTemp); } 例1 Funcenstein1函数代码 在函数Funcenstein1中,我们使用了try-finally程序块。但是它们并没有为我们做多少工作:等待一个指示灯信号,改变保护数据的内容,将新的数据指定给一个局域变量dwTemp,释放指示灯信号,返回新的数据给调用函数。 2.2.2、例2--Funcenstein2 现在让我们对Funcenstein1稍稍做一些改动,看看会出现什么情况(见清单二)。 DWORD Funcenstein2(void) { DWORD dwTemp; // 1. Do any processing here. . . . __try { // 2. request permission to access protected data, and then use it. WaitForSingleObject(g_hSem, INFINITE); g_dwProtectedData = 5; dwTemp = g_dwProtectedData; // Return the new value. return (dwTemp); } __finally { // 3. Allow others to use protected data. ReleaseSemaphore(g_hSem, 1, NULL); } // 4. Continue processing--this code will never execute in this version. dwTemp = 9; return (dwTemp); } 例2 Funcenstein2函数代码 在函数Funcenstein2中,我们在try程序段里加入了一个return返回语句。该返回语句告诉编译器,你想离开函数Funcenstein2并返回dwTemp内的内容5给调用函数。然而,如果此返回语句被执行,本线程永远不会释放指示灯信号,其它线程也就永远不会得到该指示灯信号。你可以想象,在多线程程序中这是一个多么严重的问题。 但是,使用了中断处理句柄避免了这种情况发生。当返回语句试图离开try程序段时,编译器保证了在finally程序段内的代码得到执行。所以,finally程序段内的代码保证会在try程序段中的返回语句前执行。在函数Funcenstein2中,将调用ReleaseSemaphore放在finally程序段内保证了指示灯信号会得到释放。 在finally程序段内的代码被执行后,函数Funcenstein2立即返回。这样,因为try程序段内的return返回语句,任何finally程序段后的代码都不会被执行。因而Funcenstein2返回值是5,而不是9。 必须指出的是,当遇到例2中这种过早返回语句时,编译器需要产生额外的代码以保证finally程序段内的代码的执行。此过程称作为局域展开。当然,这必然会降低整个程序的效率。所以,你应该尽量避免使用这类代码。在后面我们会讨论关键词__leave,它可以帮助我们避免编写出现局域展开一类的代码。 2.2.3、例3--Funcenstein3 现在让我们对Funcenstein2做进一步改动,看看会出现什么情况(见例3)。 DWORD Funcenstein3(void) { DWORD dwTemp; // 1. Do any processing here. . . . __try { // 2. request permission to access protected data, and then use it. WaitForSingleObject(g_hSem, INFINITE); g_dwProtectedData = 5; dwTemp = g_dwProtectedData; // Try to jump over the finally block. goto ReturnValue; } __finally { // 3. Allow others to use protected data. ReleaseSemaphore(g_hSem, 1, NULL); } dwTemp = 9; // 4. Continue processing. ReturnValue: return (dwTemp); } 例3 Funcenstein3函数代码 在函数Funcenstein3中,当遇到goto语句时编译器会产生额外的代码以保证finally程序段内的代码得到执行。但是,这一次finally程序段后ReturnValue标签后面的代码会被执行,因为try或finally程序段内没有返回语句。函数的返回值是5。同样,由于goto语句打断了从try程序段到finally程序段的自然流程,程序的效率会降低。 2.2.4、例4--Funcfurter1 现在让我们来看中断处理真正展现其功能的一个例子。(见例4)。 DWORD Funcfurter1(void) { DWORD dwTemp; // 1. Do any processing here. . . . __try { // 2. request permission to access protected data, and then use it. WaitForSingleObject(g_hSem, INFINITE); dwTemp = Funcinator(g_dwProtectedData); } __finally { // 3. Allow others to use protected data. ReleaseSemaphore(g_hSem, 1, NULL); } // 4. Continue processing. return (dwTemp); } 例4 Funcfurter1函数代码 设想try程序段内调用的Funcinator函数具有某种缺陷而造成无效内存读写。在16位视窗应用程序中,这会导致一个已定义好的错误信息对话框出现。在用户关闭对话框的同时该应用程序也终止运行。在不具有try-finally的Win32应用程序中,这会导致程序终止运行,指示灯信号永远不会得到释放。这就造成了等待该指示灯信号的其它线程会永远等待下去。而将ReleaseSemaphore放在finally程序段内则从根本上保证了不论何种情况出现指示灯信号都会得到释放。 如果中断处理句柄能够处理由于无效内存读写而造成的程序中断,我们就完全有理由相信它能够处理诸如setjump/longjump、break和continue这类的中断转移。事实也正是这样。 2.3、小测试 下面一个例子(见清单五)请读者猜测一下函数FuncaDoodleDoo的返回值。(答案为14) DWORD FuncaDoodleDoo(void) { DWORD dwTemp = 0; while (dwTemp 〈 10) { __try { if (dwTemp == 2) continue; if (dwTemp == 3) break; } __finally { dwTemp++; } dwTemp++; } dwTemp += 10; return (dwTemp); } FuncaDoodleDoo函数代码 虽然中断处理句柄能够接收出现在try程序段内的绝大部分异常情况,但是如果线程或进程中断执行的话,则finally程序段内的代码不会被执行。调用ExitThread或ExitProcess就会立即造成线程或进程的中断,而不会执行finally程序段。另外,如果其它的应用程序调用ExitThread或ExitProcess而造成你的线程或进程中断,你程序中的finally程序段也不会被执行。一些C函数如abort会调用ExitProcess,也会导致你的finally程序段不被执行。对此你无能为力。但你可以防止你自己提早调用ExitThread或ExitProcess。 2.4、应用例子 我们已经讨论了中断处理句柄的句法及语法。现在我们进一步讨论如何利用中断处理句柄来简化一个比较复杂的编程问题。 首先让我们来看一个没有使用中断处理句柄的例子,程序源代码见例6。 BOOL Funcarama1 (void) { HANDLE hFile = INVALID_HANDLE_VALUE; LPVOID lpBuf = NULL; DWORD dwNumBytesRead; BOOL fOk; hFile = CreateFile("SOMEDATA.DAT", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL); if (hFile == INVALID_HANDLE_VALUE) { return (FALSE); } lpBuf = VitualAlloc(NULL, 1024, MEM_COMMIT, PAGE_READWRITE); if (lpBuf == NULL) { CloseHandle(hFile); return (FALSE); } fOk = ReadFile(hFile, lpBuf, 1024, &dwNumBytesRead, NULL); if (!fOk || (dwNumBytesRead == 0)) { VirtualFree(lpBuf, MEM_RELEASE | MEM_DECOMMIT); CloseHandle(hFile); return (FALSE); } // Do some calculation on the data. . . . // Clean up all the resources. VirtualFree(lpBuf, MEM_RELEASE | MEM_DECOMMIT); CloseHandle(hFile); return (TRUE); } 例6 没有使用中断处理句柄的Funcarama1函数代码 在上例Funcarama1函数中,所有的错误诊断使得该函数难以理解、维护和修改。当然,我们可以对Funcarama1函数进行一些改动,使其易于理解(见例7)。 BOOL Funcarama2 (void) { HANDLE hFile = INVALID_HANDLE_VALUE; LPVOID lpBuf = NULL; DWORD dwNumBytesRead; BOOL fOk, fSuccess = FALSE; hFile = CreateFile("SOMEDATA.DAT", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL); if (hFile != INVALID_HANDLE_VALUE) { lpBuf = VitualAlloc(NULL, 1024, MEM_COMMIT, PAGE_READWRITE); if (lpBuf != NULL) { fOk = ReadFile(hFile, lpBuf, 1024, &dwNumBytesRead, NULL); if (fOk || (dwNumBytesRead != 0)) { // Do some calculation on the data. . . . fSuccess = TRUE; } } VirtualFree(lpBuf, MEM_RELEASE | MEM_DECOMMIT); } CloseHandle(hFile); return (fSuccess); } 例7 没有使用中断处理句柄的Funcarama2函数代码 虽然函数Funcarama2容易理解,但是仍然难于维护和修改。 现在让我们来利用中断处理句柄重写Funcaram1函数,其代码如清单八。 BOOL Funcarama3 (void) { HANDLE hFile = INVALID_HANDLE_VALUE; LPVOID lpBuf = NULL; __try { DWORD dwNumBytesRead; BOOL fOk; hFile = CreateFile("SOMEDATA.DAT", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL); if (hFile == INVALID_HANDLE_VALUE) { return (FALSE); } lpBuf = VitualAlloc(NULL, 1024, MEM_COMMIT, PAGE_READWRITE); if (lpBuf == NULL) { return (FALSE); } fOk = ReadFile(hFile, lpBuf, 1024, &dwNumBytesRead, NULL); if (!fOk || (dwNumBytesRead == 0)) { VirtualFree(lpBuf, MEM_RELEASE | MEM_DECOMMIT); return (FALSE); } // Do some calculation on the data. . . . } __finally { // Clean up all the resources. if (lpBuf != NULL) VirtualFree(lpBuf, MEM_RELEASE | MEM_DECOMMIT); if (hFile != INVALID_HANDLE_VALUE) CloseHandle(hFile); } // Continue processing. return (TRUE); } 例8 使用了中断处理句柄的Funcarama3函数代码 Funcarama3函数版的好处是所有的清除工作都集中在一个地方:finally程序段内。这样在我们需要对该函数增加新的条件语句时,我们只需要在finally程序段内简单增添一行清除语句就可以了,而不必回过头来在每一出可能出错的地方添加清除语句。 Funcarama3函数的真正问题在于其效率。我们以前说过应尽可能的避免在try程序段内使用return语句。为了避免这种情况,微软在它的编译器里引进了另一个关键词__leave。利用关键词__leave重写的Funcarama3函数见例9。 BOOL Funcarama4 (void) { HANDLE hFile = INVALID_HANDLE_VALUE; LPVOID lpBuf = NULL; // Assume that the function will not execute successfully. BOOL fFunctionOk = FALSE; __try { DWORD dwNumBytesRead; BOOL fOk; hFile = CreateFile("SOMEDATA.DAT", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL); if (hFile == INVALID_HANDLE_VALUE) { __leave; } lpBuf = VitualAlloc(NULL, 1024, MEM_COMMIT, PAGE_READWRITE); if (lpBuf == NULL) { __leave; } fOk = ReadFile(hFile, lpBuf, 1024, &dwNumBytesRead, NULL); if (!fOk || (dwNumBytesRead == 0)) { VirtualFree(lpBuf, MEM_RELEASE | MEM_DECOMMIT); __leave; } // Do some calculation on the data. . . . // Indicate that the entire function executed successfully. fFunctionOk = TRUE; } __finally { // Clean up all the resources. if (lpBuf != NULL) VirtualFree(lpBuf, MEM_RELEASE | MEM_DECOMMIT); if (hFile != INVALID_HANDLE_VALUE) CloseHandle(hFile); } // Continue processing. return (fFunctionOk); } 例9 使用了中断处理句柄和关键词__leave的Funcarama4函数代码 try程序段内的关键词__leave导致程序运行指针直接跳到try程序段的结尾(你可以将此看成为跳到try程序段的结束花括弧)。这样,因为控制流程将“自然”的离开try程序段,进入finally程序段,所以不需付出额外代价而导致效率降低。但是你需要引进一个新的变量来指示整个函数的运行是否成功。 从try程序段到finally程序段,控制流程既可以是自然进入,也可以是由于异常的出现而导致控制流程过早离开try程序段而进入finally程序段。为确定何种情况下造成finally程序段的运行,我们可以调用AbnormalTermination函数来诊断。 BOOL AbnormalTermination(VOID); 该函数只能在finally程序段内调用以诊断与此finally相对应的try程序段是否是过早离开。如果AbnormalTermination的返回值是FALSE,表明程序流程是自然离开try程序段。否则,则是过早离开。 3、异常处理句柄(Exception handler) 3.1、异常(或例外)处理句柄的定义 异常(或例外)是你不希望出现的事件。在一个完好的应用程序中,你不希望读写无效内存地址或除数为零的情况出现。但是这类错误的确会发生。在出现这类错误时,CPU会负责提出针对该类错误的例外。当CPU提出一个例外时,我们称之为硬件异常(或例外)(hardware exception)。操作系统和应用程序自身也可以提出自己的异常。这类异常我们称之为软件异常(或例外)(software exception)。 当一个硬件异常或软件异常被提出时,操作系统向你的程序提供一种机会使得你的程序可以诊断那类异常被提出并允许你的程序对此进行处理。异常处理句柄的语法为 __try { // Guarded body . . . } __except (exception filter) { // Exception handler . . . } 请注意关键词__except。当你建立一个try程序段时,它必须跟随一个finally程序段或一个except程序段。一个try程序段不能同时既跟随一个finally程序段又跟随一个except程序段。一个try程序段也不能同时跟随多个finally程序段或多个except程序段。但是,try-finally程序段却可以嵌套在try-except程序段内,或try-except程序段嵌套在try-finally程序段内。 3.2、几个例子 不同于中断处理句柄,异常处理句柄直接由操作系统执行,编译器不需要做太多工作。下面我们通过几个具体例子来讨论异常处理句柄是如何工作的。 3.2.1、例5--Funcmeister1函数 下面是一个使用了try-except异常处理句柄的函数Funcmeister1,其代码见清单十。 DWORD Funcmeister1 (void) { DWORD dwTemp; // 1. Do any processing here. . . . __try { // 2. Perform some operation. dwTemp = 0; } __except (EXCEPTION_EXECUTE_HANDLER) { // 3. Handle an exception; this never executes. . . . } // 3. Continue processing. return (dwTemp); } 例10 例5Funcmeister1函数代码 在Funcmeister1函数中的try程序段内,我们简单地将dwTemp赋值为零。该操作不会导致任何异常的提出。所以,except程序段内的程序永远不会被执行。请注意,这有别于中断处理句柄try-finally。在执行了dwTemp赋值语句后的下一个执行语句是return返回语句。 虽然我们不鼓励在try程序段内使用return, goto, continue和break语句,但是在异常处理句柄的try程序段内使用这些语句不会象中断处理句柄那样造成运行代码的增加和效率下降。 3.2.2、例6--Funcmeister2函数 让我们对Funcmeister1函数进行一些改动,看看会出现什么情况。改动后的函数见例11。 DWORD Funcmeister2 (void) { DWORD dwTemp = 0; // 1. Do any processing here. . . . __try { // 2. Perform some operation(s). dwTemp = 5 / dwTemp; // Generate an exception dwTemp += 10; // Never excutes } __except ( /* 3. Evaluate filter. */ EXCEPTION_EXECUTE_HANDLER) { // 4. Handle an exception; this never executes. MessageBeep(0); . . . } // 5. Continue processing. return (dwTemp); } 例11 例6Funcmeister2函数代码 函数Funcmeister2中的try程序段dwTemp = 5 / dwTemp语句导致CPU提出一个硬件异常。当该异常被提出时,操作系统会寻找相对应的except程序段的起始位置并评估其异常筛选表达式(exception filter expression)。异常筛选表达式可以取下列标识符值之一。这些标识符定义在Win32 EXCPT.H头文件中。 标识符 定义为 EXCEPTION_EXECUTE_HANDLER 1 EXCEPTION_CONTINUE_SEARCH 0 EXCEPTION_CONTINUE_EXECUTION -1 3.3、异常筛选(exception filter) EXCEPTION_EXECUTE_HANDLER表明当一个异常出现时,运行程序跳到except程序段转而执行except程序段内的代码。except程序段内的代码执行完后,系统认为该异常已处理完,接着继续执行except程序段后的代码。 EXCEPTION_CONTINUE_EXECUTION表明当一个异常出现时,运行程序不立即执行except程序段内的代码而返回try程序段内产生异常的语句继续执行该语句。 EXCEPTION_CONTINUE_SEARCH表明当一个异常出现时,运行程序不执行该except程序段内的代码而寻求由高一级的异常处理句柄来处理此异常。 Win32 WINBASE.H头文件中定义了可能出现的各种异常代码。我们可以通过调用GetExceptionCode函数来诊断何种异常被提出,从而决定异常处理句柄该采取何种行动。GetExceptionCode函数定义为 DWORD GetExceptionCode(VOID); 它的返回值表明何种异常出现。下面的程序说明如何调用GetExceptionCode函数。 __try { x = 0; y = 4 / x; } __except ((GetExceptionCode() == EXCEPTION_INT_DIVIDE_BY_ZERO) ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH) { // Handle divide by zero exception. } 当一个异常发生时,操作系统会将有关该异常的信息储存在三个结构中,并将它们存放在提出此异常线程的堆栈里。这三个结构是EXCEPTION_RECORD,CONTEXT,和EXCEPTION_POINTERS。EXCEPTION_RECORD储存着与CPU无关的异常信息,CONTEXT则储存着与CPU有关的异常信息。EXCEPTION_POINTERS结构包含了两个分别指向EXCEPTION_RECORD和CONTEXT的指针。 typedef struct _EXCEPTION_POINTERS { PEXCEPTION_RECORD ExceptionRecord; PCONTEXT ContextRecord; } EXCEPTION_POINTERS 假如你的程序需要这些异常信息,你可以通过调用GetExceptionInformation函数来获取。 LPEXCEPTION GetExceptionInformation(void); GetExceptionInformation函数返回一个指向EXCEPTION_POINTERS结构的指针。下面的函数说明了如何调用GetExceptionInformation函数。 void FuncSkunk (void) { // Declare variables that we can use to save the exception // record and the context if an exception should occur. EXCEPTION_RECORD SavedExceptRec; CONTEXT SavedContext; . . . __try { . . . } __except ( SavedExceptRec = *(GetExceptionInformation())->ExceptionRecord, SavedContext = *(GetExceptionInformation())->ContextRecord, EXCEPTION_EXECUTE_HANDLER) { // We can use the SavedExceptRec and SavedContext // variables inside the handler code block. switch (SavedExceptRec.ExceptionCode) { . . . } } . . . } 注意,在上面的异常筛选表达式程序中我们使用了C语言的“,”操作符。许多程序员对此并不是很熟悉。该操作符告诉编译器从左到右运行由“,”分离的各表达式。在所有的表达式都运行完后,返回最后一个(或最右面的)表达式的值。 4、软件异常(software exception) 至此为止我们所讨论的是如何处理由CPU提出的硬件异常(hardware exception)。通常,操作系统或应用程序自身提出的软件异常也非常有用。例如,HeapAlloc函数就提供了一个非常好的利用软件异常的例子。在调用HeapAlloc时,你可以设置HEAP_GENERATE-EXCEPTIONS指示旗(flag)。这样如果HeapAlloc不能满足你的内存分配要求,它会产生一个STATUS_NO_MEMORY软件异常。 假如你想利用这个异常,你可以在你的try程序段内继续编写你的代码,如同内存分配总是会成功一样。如果内存分配失败,你可以利用except程序段来处理这个异常或利用finally程序段来做清除工作。 你的程序不需要知道你要处理的异常是软件异常还是硬件异常。你利用try-finally和try-except来处理软件异常和硬件异常的方式是一样的。但是你可以让你的程序象HeapAlloc函数一样提出自己的异常。为了在你的程序中提出软件异常,你需要调用RaiseException函数。 VOID RaiseException(DWORD dwExceptionCode, DWORD dwExceptionFlags, DWORD cArguments, LPDWORD lpArguments); 关于该函数的使用,请参考微软的有关文献。 5、结论 结构异常处理由中断处理和例外处理两部分组成。采用结构异常处理使得你可以将精力集中在你的程序应用代码设计上,从而使得应用方案的设计更方便、具体。采用结构异常处理编写的程序更易于理解、修改和维护,从而增加了程序的可读性和维护性。
|