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) {
例2 Funcenstein2函數代碼
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);
}
在函數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.
.
.
.