[转]C++异常处理 8
微软在Wi n d o w s中引入S E H的主要动机是为了便于操作系统本身的开发。操作系统的开发人员使用S E H,使得系统更加强壮。我们也可以使用S E H,使我们的自己的程序更加强壮。
使用S E H所造成的负担主要由编译程序来承担,而不是由操作系统承担。
当异常块(exception block)出现时,编译程序要生成特殊的代码。编译程序必须产生一些表( t a b l e)来支持处理S E H的数据结构。
编译程序还必须提供回调( c a l l b a c k)函数,操作系统可以调用这些函数,保证异常块被处理。
编译程序还要负责准备栈结构和其他内部信息,供操作系统使用和参考。
在编译程序中增加S E H支持不是一件容易的事。不同的编译程序厂商会以不同的方式实现S E H,这一点并不让人感到奇怪。幸亏我们可以不必考虑编译程序的实现细节,而只使用编译程序的S E H功能。(其实大多数编译程序厂商都采用微软建议的语法)
结束处理程序代码初步
一个结束处理程序能够确保去调用和执行一个代码块(结束处理程序,termination handler),
而不管另外一段代码(保护体, guarded body)是如何退出的。结束处理程序的语法结构如下:
__try
{
file://保护块
}
__finally
{
file://结束处理程序
}
在上面的代码段中,操作系统和编译程序共同来确保结束处理程序中的__f i n a l l y代码块能够被执行,不管保护体(t r y块)是如何退出的。不论你在保护体中使用r e t u r n,还是g o t o,或者是longjump,结束处理程序(f i n a l l y块)都将被调用。
=====================
************************
我们来看一个实列:(返回值:10, 没有Leak,性能消耗:小)
DWORD Func_SEHTerminateHandle()
{
DWORD dwReturnData = 0;
HANDLE hSem = NULL;
const char* lpSemName = "TermSem";
hSem = CreateSemaphore(NULL, 1, 1, lpSemName);
__try
{
WaitForSingleObject(hSem,INFINITE);
dwReturnData = 5;
}
__finally
{
ReleaseSemaphore(hSem,1,NULL);
CloseHandle(hSem);
}
dwReturnData += 5;
return dwReturnData;
}
这段代码应该只是做为一个基础函数,我们将在后面修改它,来看看结束处理程序的作用:
====================
在代码加一句:(返回值:5, 没有Leak,性能消耗:中下)
DWORD Func_SEHTerminateHandle()
{
DWORD dwReturnData = 0;
HANDLE hSem = NULL;
const char* lpSemName = "TermSem";
hSem = CreateSemaphore(NULL, 1, 1, lpSemName);
__try
{
WaitForSingleObject(hSem,INFINITE);
dwReturnData = 5;
return dwReturnData;
}
__finally
{
ReleaseSemaphore(hSem,1,NULL);
CloseHandle(hSem);
}
dwReturnData += 5;
return dwReturnData;
}
在t r y块的末尾增加了一个r e t u r n语句。这个r e t u r n语句告诉编译程序在这里要退出这个函数并返回d w Te m p变量的内容,现在这个变量的值是5。但是,如果这个r e t u r n
语句被执行,该线程将不会释放信标,其他线程也就不能再获得对信标的控制。可以想象,这样的执行次序会产生很大的问题,那些等待信标的线程可能永远不会恢复执行。
通过使用结束处理程序,可以避免r e t u r n语句的过早执行。当r e t u r n语句试图退出t r y块时,编译程序要确保f i n a l l y块中的代码首先被执行。要保证f i n a l l y块中的代码在t r y块中的r e t u r n语句退出之前执行。在程序中,将R e l e a s e S e m a p h o r e的调用放在结束处理程序块中,保证信标总会被释放。这样就不会造成一个线程一直占有信标,否则将意味着所有其他等待信标的线程永远不会被分配C P U时间。
在f i n a l l y块中的代码执行之后,函数实际上就返回。任何出现在f i n a l l y块之下的代码将不再执行,因为函数已在t r y块中返回。所以这个函数的返回值是5,而不是10。
读者可能要问编译程序是如何保证在t r y块可以退出之前执行f i n a l l y块的。当编译程序检查源代码时,它看到在t r y块中有r e t u r n语句。这样,编译程序就生成代码将返回值(本例中是5)保存在一个编译程序建立的临时变量中。编译程序然后再生成代码来执行f i n a l l y块中包含的指令,这称为局部展开。更特殊的情况是,由于t r y块中存在过早退出的代码,从而产生局部展开,导致系统执行f i n a l l y块中的内容。在f i n a l l y块中的指令执行之后,编译程序临时变量的值被取出并从函数中返回。
可以看到,要完成这些事情,编译程序必须生成附加的代码,系统要执行额外的工作。在
不同的C P U上,结束处理所需要的步骤也不同。例如,在A l p h a处理器上,必须执行几百个甚至几千个C P U指令来捕捉t r y块中的过早返回并调用f i n a l l y块。在编写代码时,就应该避免引起结束处理程序的t r y块中的过早退出,因为程序的性能会受到影响。
后面,将讨论_ _ l e a v e关键字,它有助于避免编写引起局部展开的代码。
设计异常处理的目的是用来捕捉异常的—不常发生的语法规则的异常情况(在我们的例子中,就是过早返回)。如果情况是正常的,明确地检查这些情况,比起依赖操作系统和编译程序的S E H功能来捕捉常见的事情要更有效。
注意当控制流自然地离开t r y块并进入f i n a l l y块(就像在F u n c e n s t e i n 1中)时,进入f i n a l l y块的系统开销是最小的。在x86 CPU上使用微软的编译程序,当执行离开try 块进入f i n a l l y块时,只有一个机器指令被执行,读者可以在自己的程序中注意到这种系统开销。当编译程序要生成额外的代码,系统要执行额外的工作时系统开销就很值得注意了
========================
修改代码:(返回值:5,没有Leak,性能消耗:中)
DWORD Func_SEHTerminateHandle()
{
DWORD dwReturnData = 0;
HANDLE hSem = NULL;
const char* lpSemName = "TermSem";
hSem = CreateSemaphore(NULL, 1, 1, lpSemName);
__try
{
WaitForSingleObject(hSem,INFINITE);
dwReturnData = 5;
if(dwReturnData == 5)
goto ReturnValue;
return dwReturnData;
}
__finally
{
ReleaseSemaphore(hSem,1,NULL);
CloseHandle(hSem);
}
dwReturnData += 5;
ReturnValue:
return dwReturnData;
}
代码中,当编译程序看到t r y块中的g o t o语句,它首先生成一个局部展开来执行f i n a l l y块中的内容。这一次,在f i n a l l y块中的代码执行之后,在R e t u r n Va l u e标号之后的代码将执行,因为在t r y块和f i n a l l y块中都没有返回发生。这里的代码使函数返回5。而且,由于中断了从t r y块到f i n a l l y块的自然流程,可能要蒙受很大的性能损失(取决于运行程序的C P U)
*************************
=====================
写上面的代码是初步的,现在来看结束处理程序在我们代码里面的真正的价值:
看代码:(信号灯被正常释放,reserve的一页内存没有被Free,安全性:安全)
DWORD TermHappenSomeError()
{
DWORD dwReturnValue = 9;
DWORD dwMemorySize = 1024;
char* lpAddress;
lpAddress = (char*)VirtualAlloc(NULL, dwMemorySize, MEM_RESERVE, PAGE_READWRITE);