C++異常處理(轉)

 

和其它很多程序員一樣,本書的主人公阿愚也是在初學C++時,在C++的sample代碼中與異常處理的編程方法初次邂逅的,如下:

// Normal program statements
...

try
{
// Execute some code that might throw an exception.
}
catch( CException* e )
{
// Handle the exception here.
// "e" contains information about the exception.
e->Delete();
}

// Other normal program statements

瞧瞧,代碼看上去顯得那麼整齊、乾淨,try block和catch block遙相呼應,多有對稱美呀!因此主人公初次見面後就一見鍾情了。

爲什麼要選用異常處理的編程方法?
當然更爲重要的是,C++中引入的異常處理的編程機制提供給程序員一種全新的、更好的編程方法和思想。在C++中明確提出trycatch異常處理編程方法的框架之前的年代,程序員是怎樣編寫程序的,如下:

void main(int argc, char* argv[])
{
if (Call_Func1(in, param out)
{
// 函數調用成功,我們正常的處理
if (Call_Func2(in, param out)
{
// 函數調用成功,我們正常的處理
while(condition)
{
//do other job
if (has error)
{
// 函數調用失敗,表明程序執行過程中出現一些錯誤,
// 因此必須處理錯誤
process_error();
exit();
}
//do other job
}
}
else
{
// 函數調用失敗,表明程序執行過程中出現一些錯誤,
// 因此必須處理錯誤
process_error();
exit();
}
}
else
{
// 函數調用失敗,同樣是錯誤處理
process_error();
exit();
}
}

因爲程序的執行過程中總會遇到許多可預知或不可預知的錯誤事件,例如說,由於內存資源有限導致需要分配的內存失敗了;或某個目錄下本應存在的一個文件找不着了;或說不小心被零除了、內存越界了、數組越界了等等。這些錯誤事件存在非常大的隱患,因此程序員總需要在程序中不斷加入if語句,來判斷是否有異常出現,如果有,就必須要及時處理,否則可能帶來意想不到的,甚至是災難性的後果。這樣一來,程序可讀性差了很多,總是有許多與真正工作無關的代碼,而且也給程序員增加了極大的工作負擔,多數類似的處理錯誤的代碼模塊就像滿山的牛屎一樣遍地都是(程序員不大多是“牛”人嗎?所以。。。哈哈)。
但C++中的異常處理的機制徹底改變了這種面貌,它使真正的計算處理和錯誤處理分開來,讓程序員不再被這些瑣碎的事情所煩擾,能關注於真正的計算處理工作。同時代碼的可讀性也好了。因此我們有理由選擇異常處理的編程方法。具體原因如下:
1、 把錯誤處理和真正的工作分開來;
2、 代碼更易組織,更清晰,複雜的工作任務更容易實現;
3、 毫無疑問,更安全了,不至於由於一些小的疏忽而使程序意外崩潰了;
4、 由於C++中的try catch可以分層嵌套,所以它提供了一種方法使得程序的控制流可以安全的跳轉到上層(或者上上層)的錯誤處理模塊中去。(不同於return語句,異常處理的控制流是可以安全地跨越一個或多個函數 )。
5、 還有一個重要的原因就是,由於目前需要開發的軟件產品總是變得越來越複雜、越來越龐大,如果系統中沒有一個可靠的異常處理模型,那必定是一件十分糟糕的局面。

相信絕大多數程序員都知道C++中的異常處理的編程方法,可還是有很多人已習慣原來單純的面向過程的代碼組織方式,不太習慣或較少使用trycatch異常處理。爲了使您編寫的代碼更安全;爲了使您編寫的代碼讓他人更易閱讀,主人公阿愚強烈建議在您書寫的代碼中儘可能多用異常處理機制,少一些不必要的if判斷語句。

 

如果您喜歡玩一款遊戲,您必須先要很好理解這款遊戲的規則。同樣主人公阿愚喜歡上C++中異常處理後,當然也首先關注它的遊戲規則,這就是C++中異常處理的語法。

關鍵字
1、 try
2、 catch
3、 throw
其中關鍵字try表示定義一個受到監控、受到保護的程序代碼塊;關鍵字catch與try遙相呼應,定義當try block(受監控的程序塊)出現異常時,錯誤處理的程序模塊,並且每個catch block都帶一個參數(類似於函數定義時的數那樣),這個參數的數據類型用於異常對象的數據類型進行匹配;而throw則是檢測到一個異常錯誤發生後向外拋出一個異常事件,通知對應的catch程序塊執行對應的錯誤處理。

語法
1、還是給一個例子吧!如下:
int main()
{
cout << "In main." << endl;

//定義一個try block,它是用一對花括號{}所括起來的塊作用域的代碼塊
try
{
cout << "在 try block 中, 準備拋出一個異常." << endl;

//這裏拋出一個異常(其中異常對象的數據類型是int,值爲1)
//由於在try block中的代碼是受到監控保護的,所以拋出異常後,程序的
//控制流便轉到隨後的catch block中
throw 1;

cout << "在 try block 中, 由於前面拋出了一個異常,因此這裏的代碼是不會得以執行到的" << endl;
}
//這裏必須相對應地,至少定義一個catch block,同樣它也是用花括號括起來的
catch( int& value )
{
cout << "在 catch block 中, 處理異常錯誤。異常對象value的值爲:"<< value << endl;
}

cout << "Back in main. Execution resumes here." << endl;
return 0;

}

2、語法很簡單吧!的確如此。另外一個try block可以有多個對應的catch block,可爲什麼要多個catch block呢?這是因爲每個catch block匹配一種類型的異常錯誤對象的處理,多個catch block呢就可以針對不同的異常錯誤類型分別處理。畢竟異常錯誤也是分級別的呀!有致命的、有一般的、有警告的,甚至還有的只是事件通知。例子如下:
int main()
{
try
{
cout << "在 try block 中, 準備拋出一個int數據類型的異常." << endl;
throw 1;

cout << "在 try block 中, 準備拋出一個double數據類型的異常." << endl;
throw 0.5;
}
catch( int& value )
{
cout << "在 catch block 中, int數據類型處理異常錯誤。”<< endl;
}
catch( double& d_value )
{
cout << "在 catch block 中, double數據類型處理異常錯誤。”<< endl;
}

return 0;
}

3、一個函數中可以有多個trycatch結構塊,例子如下:
int main()
{
try
{
cout << "在 try block 中, 準備拋出一個int數據類型的異常." << endl;
throw 1;
}
catch( int& value )
{
cout << "在 catch block 中, int數據類型處理異常錯誤。”<< endl;
}

//這裏是二個trycatch結構塊,當然也可以有第三、第四個,甚至更多
try
{
cout << "在 try block 中, 準備拋出一個double數據類型的異常." << endl;
throw 0.5;
}
catch( double& d_value )
{
cout << "在 catch block 中, double數據類型處理異常錯誤。”<< endl;
}

return 0;
}

4、上面提到一個try block可以有多個對應的catch block,這樣便於不同的異常錯誤分類處理,其實這只是異常錯誤分類處理的方法之一(暫且把它叫做橫向展開的吧!)。另外還有一種就是縱向的,也即是分層的、trycatch塊是可以嵌套的,當在低層的trycatch結構塊中不能匹配到相同類型的catch block時,它就會到上層的trycatch塊中去尋找匹配到正確的catch block異常處理模塊。例程如下:
int main()
{
try
{
//這裏是嵌套的trycatch結構塊
try
{
cout << "在 try block 中, 準備拋出一個int數據類型的異常." << endl;
throw 1;
}
catch( int& value )
{
cout << "在 catch block 中, int數據類型處理異常錯誤。”<< endl;
}

cout << "在 try block 中, 準備拋出一個double數據類型的異常." << endl;
throw 0.5;
}
catch( double& d_value )
{
cout << "在 catch block 中, double數據類型處理異常錯誤。”<< endl;
}

return 0;
}

5、講到是trycatch塊是可以嵌套分層的,並且通過異常對象的數據類型來進行匹配,以找到正確的catch block異常錯誤處理代碼。這裏就不得不詳細敘述一下通過異常對象的數據類型來進行匹配找到正確的catch block的過程。
(1) 首先在拋出異常的trycatch塊中查找catch block,按順序先是與第一個catch block塊匹配,如果拋出的異常對象的數據類型與catch block中傳入的異常對象的臨時變量(就是catch語句後面參數)的數據類型完全相同,或是它的子類型對象,則匹配成功,進入到catch block中執行;否則到二步;
(2) 如果有二個或更多的catch block,則繼續查找匹配第二個、第三個,乃至最後一個catch block,如匹配成功,則進入到對應的catch block中執行;否則到三步;
(3) 返回到上一級的trycatch塊中,按規則繼續查找對應的catch block。如果找到,進入到對應的catch block中執行;否則到四步;
(4) 再到上上級的trycatch塊中,如此不斷遞歸,直到匹配到頂級的trycatch塊中的最後一個catch block,如果找到,進入到對應的catch block中執行;否則程序將會執行terminate()退出。
另外分層嵌套的trycatch塊是可以跨越函數作用域的,例程如下:
void Func() throw()
{
//這裏實際上也是嵌套在裏層的trycatch結構塊
try
{
cout << "在 try block 中, 準備拋出一個int數據類型的異常." << endl;
//由於這個trycatch塊中不能找到匹配的catch block,所以
//它會繼續查找到調用這個函數的上層函數的trycatch塊。
throw 1;
}
catch( float& value )
{
cout << "在 catch block 中, int數據類型處理異常錯誤。”<< endl;
}
}

int main()
{
try
{
Func();

cout << "在 try block 中, 準備拋出一個double數據類型的異常." << endl;
throw 0.5;
}
catch( double& d_value )
{
cout << "在 catch block 中, double數據類型處理異常錯誤。”<< endl;
}
catch( int& value )
{
//這個例子中,Func()函數中拋出的異常會在此被處理
cout << "在 catch block 中, int數據類型處理異常錯誤。”<< endl;
}

return 0;
}

6、剛纔提到,嵌套的trycatch塊是可以跨越函數作用域的,其實這裏面還有另外一層涵義,就是拋出異常對象的函數中並不一定必須存在trycatch塊,它可以是調用這個函數的上層函數中存在trycatch塊,這樣這個函數的代碼也同樣是受保護、受監控的代碼;當然即便是上層調用函數不存在trycatch塊,也只是不能找到處理這類異常對象錯誤處理的catch block而已,例程如下:
void Func() throw()
{
//這裏實際上也是嵌套在裏層的trycatch結構塊
//由於這個函數中是沒有trycatch塊的,所以它會查找到調用這個函數的上
//層函數的trycatch塊中。
throw 1;
}

int main()
{
try
{
//調用函數,注意這個函數裏面拋出一個異常對象
Func();

cout << "在 try block 中, 準備拋出一個double數據類型的異常." << endl;
throw 0.5;
}
catch( double& d_value )
{
cout << "在 catch block 中, double數據類型處理異常錯誤。”<< endl;
}
catch( int& value )
{
//這個例子中,Func()函數中拋出的異常會在此被處理
cout << "在 catch block 中, int數據類型處理異常錯誤。”<< endl;
}

//如果這裏調用這個函數,那麼由於main()已經是調用棧的頂層函數,因此不能找
//到對應的catch block,所以程序會執行terminate()退出。
Func();
// [特別提示]:在C++標準中規定,可以在程序任何地方throw一個異常對象,
// 並不要求一定只能是在受到try block監控保護的作用域中才能拋出異常,但
// 如果在程序中出現了拋出的找不到對應catch block的異常對象時,C++標
// 準中規定要求系統必須執行terminate()來終止程序。
// 因此這個例程是可以編譯通過的,但運行時卻會異常終止。這往往給軟件
// 系統帶來了不安全性。與此形成對比的是java中提供的異常處理模型卻是不
// 永許出現這樣的找不到對應catch block的異常對象,它在編譯時就給出錯誤
// 提示,所以java中提供的異常處理模型往往比C++要更完善,後面的章節
// 會進一步對這兩種異常處理模型進行一個詳細的分析比較。
return 0;
}

朋友們!C++中的異常處理模型的語法很簡單吧!就是那麼(one、two、three、…哈哈!數數呢!)簡單的幾條規則。怪不得主人公阿愚這麼快就喜歡上她了,而且還居然像一個思想家一樣總結出一條感想:好的東西往往都是簡單的,簡單就是美嗎!哈哈!還挺臭美的。

上一篇文章中詳細講了講C++異常處理模型的trycatch使用語法,其中catch關鍵字是用來定義catch block的,它後面帶一個參數,用來與異常對象的數據類型進行匹配。注意catch關鍵字只能定義一個參數,因此每個catch block只能是一種數據類型的異常對象的錯誤處理模塊。如果要想使一個catch block能抓獲多種數據類型的異常對象的話,怎麼辦?C++標準中定義了一種特殊的catch用法,那就是” catch(…)”。

感性認識
1、catch(…)到底是一個什麼樣的東東,先來個感性認識吧!看例子先:

int main()
{
try
{
cout << "在 try block 中, 準備拋出一個異常." << endl;
//這裏拋出一個異常(其中異常對象的數據類型是int,值爲1)
throw 1;
}
//catch( int& value )
//注意這裏catch語句
catch( …)
{
cout << "在 catch(…) block 中, 拋出的int類型的異常對象被處理" << endl;
}
}

2、哈哈!int類型的異常被catch(…)抓獲了,再來另一個例子:

int main()
{
try
{
cout << "在 try block 中, 準備拋出一個異常." << endl;
//這裏拋出一個異常(其中異常對象的數據類型是double,值爲0.5)
throw 0.5;
}
//catch( double& value )
//注意這裏catch語句
catch( …)
{
cout << "在 catch(…) block 中, double類型的異常對象也被處理" << endl;
}
}

3、同樣,double類型的異常對象也被catch(…)塊抓獲了。是的,catch(..)能匹配成功所有的數據類型的異常對象,包括C++語言提供所有的原生數據類型的異常對象,如int、double,還有char*、int*這樣的指針類型,另外還有數組類型的異常對象。同時也包括所有自定義的抽象數據類型。例程如下:

int main()
{
try
{
cout << "在 try block 中, 準備拋出一個異常." << endl;
//這裏拋出一個異常(其中異常對象的數據類型是char*)
char* p=0;
throw p;
}
//catch( char* value )
//注意這裏catch語句
catch( …)
{
cout << "在 catch(…) block 中, char*類型的異常對象也被處理" << endl;
}
}

int main()
{
try
{
cout << "在 try block 中, 準備拋出一個異常." << endl;
//這裏拋出一個異常(其中異常對象的數據類型是int[])
int a[4];
throw a;
}
//catch( int value[] )
//注意這裏catch語句
catch( …)
{
cout << "在 catch(…) block 中, int[]類型的異常對象也被處理" << endl;
}
}

4、對於抽象數據類型的異常對象。catch(…)同樣有效,例程如下:

class MyException
{
public:
protected:
int code;
};

int main()
{
try
{
cout << "在 try block 中, 準備拋出一個異常." << endl;
//這裏拋出一個異常(其中異常對象的數據類型是MyException)
throw MyException();
}
//catch(MyException& value )
//注意這裏catch語句
catch( …)
{
cout << "在catch(…) block中, MyException類型的異常對象被處理" << endl;
}
}
對catch(…)有點迷糊?
1、究竟對catch(…)有什麼迷糊呢?還是看例子先吧!
void main()
{
int* p = 0;

try
{
// 注意:下面這條語句雖然不是throw語句,但它在執行時會導致系統
// 出現一個存儲保護錯誤的異常(access violation exception)
*p = 13; // causes an access violation exception;
}
catch(...)
{
//catch(…)能抓獲住上面的access violation exception異常嗎?
cout << "在catch(…) block中" << endl;
}
}

請問上面的程序運行時會出現什麼結果嗎?catch(…)能抓獲住系統中出現的access violation exception異常嗎?朋友們!和我們的主人公阿愚一樣,自己動手去測試一把!
結果又如何呢?實際上它有兩種不同的運行結果,在window2000系統下用VC來測試運行這個小程序時,發現程序能輸出"在catch(…) block中"的語句在屏幕上,也即catch(…) 能成功抓獲住系統中出現的access violation exception異常,很厲害吧!但如果這個同樣的程序在linux下用gcc編譯後運行時,程序將會出現崩潰,並在屏幕上輸出”segment fault”的錯誤信息。
主人公阿愚有點急了,也開始有點迷糊了,爲什麼?爲什麼?爲什麼同樣一個程序在兩種不同的系統上有不同的表現呢?其原因就是:對於這種由於硬件或操作系統出現的系統異常(例如說被零除、內存存儲控制異常、頁錯誤等等)時,window2000系統有一個叫做結構化異常處理(Structured Exception Handling,SEH)的機制,這個東東太厲害了,它能和VC中的C++異常處理模型很好的結合上(實際上VC實現的C++異常處理模型很大程度上建立在SEH機制之上的,或者說它是SEH的擴展,後面文章中會詳細闡述並分析這個久富盛名的SEH,看看catch(…)是如何神奇接管住這種系統異常出現後的程序控制流的,不過這都是後話)。而在linux系統下,系統異常是由信號處理編程方法來控制的(信號處理編程,signal processing progamming。在介紹unix和linux下如何編程的書籍中,都會有對信號處理編程詳細的介紹,當然執著的主人公阿愚肯定對它也不會放過,會深入到unix沿襲下來的信號處理編程內部的實現機制,並嘗試完善改進它,使它也能夠較好地和C++異常處理模型結合上)。
那麼C++標準中對於這種同一個程序有不同的運行結果有何解釋呢?這裏需要注意的是,window2000系統下catch(…)能捕獲住系統異常,這完全是它自己的擴展。在C++標準中並沒有要求到這一點,它只規定catch(…)必須能捕獲程序中所有通過throw語句拋出的異常。因此上面的這個程序在linux系統下的運行結果也完全是符合C++標準的。雖然大家也必須承認window2000系統下對C++異常處理模型的這種擴展確實是一個很不錯的完善,極大得提高了程序的安全性。

爲什麼要用catch(…)這個東東?
程序員朋友們也許會說,這還有問嗎?這篇文章的一開始不就講到了嗎?catch(…)能夠捕獲多種數據類型的異常對象,所以它提供給程序員一種對異常對象更好的控制手段,使開發的軟件系統有很好的可靠性。因此一個比較有經驗的程序員通常會這樣組織編寫它的代碼模塊,如下:

void Func()
{
try
{
// 這裏的程序代碼完成真正複雜的計算工作,這些代碼在執行過程中
// 有可能拋出DataType1、DataType2和DataType3類型的異常對象。
}
catch(DataType1& d1)
{
}
catch(DataType2& d2)
{
}
catch(DataType3& d3)
{
}
// 注意上面try block中可能拋出的DataType1、DataType2和DataType3三
// 種類型的異常對象在前面都已經有對應的catch block來處理。但爲什麼
// 還要在最後再定義一個catch(…) block呢?這就是爲了有更好的安全性和
// 可靠性,避免上面的try block拋出了其它未考慮到的異常對象時導致的程
// 序出現意外崩潰的嚴重後果,而且這在用VC開發的系統上更特別有效,因
// 爲catch(…)能捕獲系統出現的異常,而系統異常往往令程序員頭痛了,現
// 在系統一般都比較複雜,而且由很多人共同開發,一不小心就會導致一個
// 指針變量指向了其它非法區域,結果意外災難不幸發生了。catch(…)爲這種
// 潛在的隱患提供了一種有效的補救措施。
catch(…)
{
}
}
還有,特別是VC程序員爲了使開發的系統有更好的可靠性,往往在應用程序的入口函數中(如MFC框架的開發環境下CXXXApp::InitInstance())和工作線程的入口函數中加上一個頂層的trycatch塊,並且使用catch(…)來捕獲一切所有的異常,如下:

BOOL CXXXApp::InitInstance()
{
if (!AfxSocketInit())
{
AfxMessageBox(IDP_SOCKETS_INIT_FAILED);
return FALSE;
}

AfxEnableControlContainer();

// Standard initialization
// If you are not using these features and wish to reduce the size
// of your final executable, you should remove from the following
// the specific initialization routines you do not need.

#ifdef _AFXDLL
Enable3dControls(); // Call this when using MFC in a shared DLL
#else
Enable3dControlsStatic(); // Call this when linking to MFC statically
#endif

// 注意這裏有一個頂層的trycatch塊,並且使用catch(…)來捕獲一切所有的異常
try
{
CXXXDlg dlg;
m_pMainWnd = &dlg;
int nResponse = dlg.DoModal();
if (nResponse == IDOK)
{
// TODO: Place code here to handle when the dialog is
// dismissed with OK
}
else if (nResponse == IDCANCEL)
{
// TODO: Place code here to handle when the dialog is
// dismissed with Cancel
}
}
catch(…)
{
// dump出系統的一些重要信息,並通知管理員查找出現意外異常的原因。
// 同時想辦法恢復系統,例如說重新啓動應用程序等
}

// Since the dialog has been closed, return FALSE so that we exit the
// application, rather than start the application's message pump.
return FALSE;
}

通過上面的例程和分析可以得出,由於catch(…)能夠捕獲所有數據類型的異常對象,所以在恰當的地方使用catch(…)確實可以使軟件系統有着更好的可靠性。這確實是大家使用catch(…)這個東東最好的理由。但不要誤會的是,在C++異常處理模型中,不只有catch(…)方法能夠捕獲幾乎所有類型的異常對象(也許有其它更好的方法,在下一篇文章中主人公阿愚帶大家一同去探討一下),可C++標準中爲什麼會想到定義這樣一個catch(…)呢?有過java或C#編程開發經驗的程序員會發現,在它們的異常處理模型中,並沒有這樣類似的一種語法,可這裏不得不再次強調的是,java中的異常處理模型是C++中的異常處理模型的完善改進版,可它反而沒有了catch(…),爲何呢?還是先去看看下一章吧,“C++的異常處理和麪向對象的緊密關係”。也許大家能找到一個似乎合理的原因

如果有人問起C++和C到底有那些本質上的不同點?主人公阿愚當然也會有自己的一份理解,他會毫不猶豫回答出:“與C相比,C++至少引入了兩項重要技術,其一就是對面向對象的全面支持;還有一項就是C++優良的異常處理模型”。是的,這兩項技術對構建出一個優良的可靠複雜的軟件系統都太重要了。可這兩項技術之間又有何關係呢?非常客觀公正的說,它們之間的關係實在是太緊密了,兩者相互支持和依賴,是構建優良可靠複雜的軟件系統最不可缺乏的兩個東東。

用對象來描述程序中出現的異常
雖然前幾篇文章的內容中列舉的一些小例子程序大多都是throw一些如int、double類型的異常,但程序員朋友都很熟悉,實際開發環境中所拋出的異常都是一個個代表抽象數據類型的對象,如C++標準庫中的std::exception(),MFC開發庫中Cexception等。用對象來描述的我們程序中的出現異常的類型和異常信息是C++異常處理模型中最閃光之處,而且這一特點一直沿用到java語言的異常處理模型中。
爲什麼要用對象來描述程序中出現的異常呢?這樣做的優勢何在?主人公阿愚不喜歡窮擺出什麼大道理,還是老辦法,從具體的實例入手。由於異常有許許多多種類型,如有致命的錯誤、一般的錯誤、警告或其它事件通知等,而且不同類型的異常有不同的處理方法,有的異常是不可恢復的,而有的異常是可以恢復的(專業術語叫做“重入”吧!哈哈,主人公阿愚有時也會來點文縐縐的東西),所以程序員在開發系統時就必須考慮把各種可能出現的異常進行分類,以便能夠分別處理。下面爲一個應用系統設計出一個對異常進行分類的簡單例子,如下:

從上面的異常分類來看,它有明顯的層次性和繼承性,這恰恰和麪向對象的繼承思想如出一轍,因此用對象來描述程序中出現的異常是再恰當不過的了。而且可以利用面向對象的特性很好的對異常進行分類處理,例如有這樣一個例子:
void OpenFile(string f)
{
try
{
// 打開文件的操作,可能拋出FileOpenException
}
catch(FileOpenException& fe)
{
// 處理這個異常,如果這個異常可以很好的得以恢復,那麼處理完畢後函數
// 正常返回;否則必須重新拋出這個異常,以供上層的調用函數來能再次處
// 理這個異常對象
int result = ReOpenFile(f);
if (result == false) throw;
}
}

void ReadFile(File f)
{
try
{
// 從文件中讀數據,可能拋出FileReadException
}
catch(FileReadException& fe)
{
// 處理這個異常,如果這個異常可以很好的得以恢復,那麼處理完畢後函數
// 正常返回;否則必須重新拋出這個異常,以供上層的調用函數來能再次處
// 理這個異常對象
int result = ReReadFile(f);
if (result == false) throw;
}
}

void WriteFile(File f)
{
try
{
// 往文件中寫數據,可能拋出FileWriteException
}
catch(FileWriteException& fe)
{
// 處理這個異常,如果這個異常可以很好的得以恢復,那麼處理完畢後函數
// 正常返回;否則必須重新拋出這個異常,以供上層的調用函數來能再次處
// 理這個異常對象
int result = ReWriteFile(f);
if (result == false) throw;
}
}

void Func()
{
try
{
// 對文件進行操作,可能出現FileWriteException、FileWriteException
// 和FileWriteException異常
OpenFile(…);

ReadFile(…);

WriteFile(…);
}
// 注意:FileException是FileOpenException、FileReadException和FileWriteException
// 的基類,因此這裏定義的catch(FileException& fe)能捕獲所有與文件操作失敗的異
// 常。
catch(FileException& fe)
{
ExceptionInfo* ef = fe.GetExceptionInfo();
cout << “操作文件時出現了不可恢復的錯誤,原因是:”<< fe << endl;
}
}

通過上面簡單的例子可以看出,利用面向對象的方法,確實能很好地對異常進行分類處理,分層處理,如果異常能得以恢復的儘可能去實現恢復,否則向上層重新拋出異常表明當前的函數不能對這裏異常進行有效恢復。同時特別值得一提的是,上層的catch block利用申明一個基類的異常對象作爲catch關鍵字的參數,使得提供了對多種類型的異常對象的集中處理方法,這就是上一篇文章中所提到的除了catch(…)以外,還有其它的來實現對多種類型的異常對象的集中處理方法,而且利用對象基類的方法顯然要比catch(…)優雅很多,方便很多,要知道在catch(…)的異常處理模塊中是沒有多少辦法獲取一些關於異常出現時異常具體信息的,而對象基類的方法則完全不同,異常處理模塊可以訪問到真正的異常對象。
現在回想一下上一篇文章中提出的那個問題?就是既然有其它很好的方法(利用類的繼承性)來可以代替catch(…)提供的異常集中處理,那爲什麼C++標準中還偏要提供catch(…)這樣一種奇怪的語法呢?其實這還是由於C++本身一些特點所決定的,因爲大家都知道,C++在業界有很多的版本,更重要的是沒有一個統一的標準開發類庫,或者說沒有統一的標準開發環境,雖然存在標準C庫和標準C++庫,但這遠遠不夠,構成不了一個完整的開發支撐環境,因此在許多重要的開發庫中都各自爲政,它們在自己的開發庫都各自定義了一套對異常進行分類支持的庫。因此應用程序的開發環境往往都同時需要依賴於幾個基礎開發庫之上(例如MFC + XML4C + Standard C++),這樣對開發人員而言,便沒有一個共同的異常對象的基類,所以C++標準中便提供了catch(…)來捕獲所有異常,這確實是一種不得已而爲之的折衷方法(哈哈!這個理解完全是主人公阿愚自己一相情願,一個人胡思亂想而出來的,朋友們如有不同的意見可以和阿愚一起討論!),另外JAVA中則不會出現這種情況,因爲JDK是統一的,所有的異常對象都是從java.lang.Throwable接口繼承而來的,因此只要在程序的入口函數中catch(java.lang.Throwable all),便可以捕獲所有的異常。所以在JAVA的異常處理模型中沒有類似C++那樣一個catch(…)的東東,完全沒必要。

異常處理中採用面向對象技術還有哪些好處呢?
上面講到,用對象來描述程序中出現的異常除了能很好地分層處理異常外,還有那些好處呢?當然除了好處大大的外,好處也是多多的,例如:
(1) 面向對象的實現中,一般都很好的實現了對象的RTTI技術,如果異常用對象來表示,那麼就可以很好完成異常對象的數據類型匹配,還有就是函數的多態,例如上面的那個例子中,即便是到了catch(FileException& fe)異常處理模塊中,也能知道到底是出現了那種具體的異常,是FileOpenException呢?還是其它的異常?
(2) 面向對象的實現中,一般都很好的實現了對象的構造、對象的銷燬、對象的轉存複製等等,所以這也爲異常處理模型中,異常對象的轉存複製和對象銷燬提供了很好的支持,容易控制;
(3) 其它的嗎?暫時沒有,可能還有沒想到的。

在異常處理的分層管理下,異常對象的重新拋出往往非常常見,本文中剛纔的例子就有這樣的情況,但當時僅一筆帶過而已,爲了表明對它的重視,下一篇文章重點討論一下異常對象的rethrow處理。

上一篇文章已經提到了C++的異常rethrow現象,這篇文章將進一步深入討論這個問題。當catch到一個異常後進入異常處理程序塊中,此時需要從傳入的異常對象中得到一些關於異常的詳細信息,並判斷這個異常是否能得以恢復,是否能在當前的異常處理程序塊中得以處理。如果是,那麼及時地處理掉這個異常,使程序恢復到正常的工作軌道上(也即從catch block後面的代碼處繼續執行);否則就必須重新拋出異常(Excption Rethrow),把這個異常交給上一層函數的異常處理模塊去處理(反正我是處理不了了,而且我也通知了我的上層領導,所以責任嗎,也就不由我擔當了,哈哈 ^-^)。

語法
很簡單,有兩種用法,如下:
1、 throw ;
2、 throw exception_obj ;
第一種表示原來的異常對象再次被重新拋出;第二中呢,則表示原來的異常已處理或正在處理中,但此時又引發了另一個異常。示例如下:

void main()
{
try
{
try
{
throw 4;
}
catch(int value)
{
// 第一種用法,原來的異常被再次拋出
// 注意它不需要帶參數。
throw;
}

try
{
throw 0.5;
}
catch(double value)
{
// 第二種用法,再次拋出另外的一個異常
// 它的語法和其它正常拋出異常的語法一樣。
throw “another exception”;
}
}
catch(...)
{
cout << “unknow exception”<< endl;
}
}
在什麼地方異常可以rethrow?
當然,異常的rethrow只能在catch block中,或者說在catch block中拋出的異常纔是異常的rethrow,因此注意下面的示例程序中存在語法錯誤,如下:
void main()
{
try
{
try
{
throw 4;
}
catch(int value)
{
// 這裏的語法是對的。
throw;
}

// 但這裏的語法卻是不對的。
// 不能在這裏進行異常的rethrow
throw;
}
catch(...)
{
cout << “unknow exception”<< endl;
}
}

異常rethrow需要注意的問題!
異常rethrow需要注意什麼問題呢?看例子先!

void main()
{
try
{
try
{
throw 4;
}
catch(int value)
{
// 異常的rethrow
throw;
}
catch(...)
{
cout << “能打印我這條消息嗎?”<< endl;
}
}
catch(...)
{
cout << “unknow exception”<< endl;
}
}
上面的程序運行結果是:“unknow exception”
由此我們可以得出結論,異常的rethrow後,它會在它上一層的trycatch塊開始查找匹配的catch block異常處理塊,而在同一層中,如果當前的catch block後面還有其它的catch block,它是不會去匹配的。所以程序中一般層次模型的trycatch要比線性結構的trycatch要好一些,如下(示例2要比示例1好):

// 示例1
void main()
{
try
{
}
catch(DataType1&)
{
}
catch(DataType2&)
{
}
catch(DataType3&)
{
}
catch(...)
{
}
}

// 示例2
void main()
{
try
{
try
{
try
{
try
{
}
catch(DataType1&)
{
}
}
catch(DataType2&)
{
}
}
catch(DataType3&)
{
}
}
catch(...)
{
}
}

總結
相遇篇的文章到此結束。通過這幾篇文章的介紹,目前已經對異常處理編程的思想,C++異常處理模型、語法,以及C++異常處理與面向對象的關係等等,都有了一個大概性的瞭解。主人公阿愚根據自己的理解和經驗,現在對相遇篇中的知識再做出如下一些總結:
(1) 異常處理編程和麪向對象,是現在程序設計和編程中最不可缺少的兩個好東東;
(2) C++異常處理模型是層次型的,能很好地支持嵌套;
(3) C++異常處理編程提供try、catch和throw三個關鍵字。其中try定義受監控的程序塊;catch定義異常處理模塊;throw讓程序員可以在程序執行出錯的地方拋出異常;
(4) C++異常處理模型的實現充分使用到了面向對象的思想和方法;
(5) C++異常處理模型中,異常是可以rethrow的。

從下篇文章開始,主人公阿愚對異常處理編程將進入到了一個相知的階段。這一階段中,阿愚將全面性地去深入瞭解異常處理編程中的各個細節和一些特點,並根據自己的理解闡述一些異常處理編程設計思想方面的東西。各位程序員朋友們,準備好了嗎?Let's go!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章