轉載:http://www.cnblogs.com/zhyg6516/archive/2011/03/08/1977007.html
C++ 用異常使得可以將正常執行代碼和出錯處理區別開來。 比如一個棧,其爲空時,調用其一個pop 函數,接下來怎麼辦? 棧本身並不知道該如何處理,需要通知給其調用者(caller),因爲只有調用者清楚接下來該怎麼做。 異常,就提供了一個很好機制。 但是異常需要操作系統,編譯器,RTTI的特性支持。
下面圍繞一個問題 “爲什麼析構函數不能拋出異常?” 展開C++中異常的實現。
Effective C++ 裏面有一條”別讓異常逃離析構函數“,大意說是Don't do that, otherwise the behavior is undefined. 這裏討論一下從異常的實現角度,討論一下爲什麼不要 ?
1. 函數調用框架和SEH( Structure Error Handling)
程序
1 int widget( int a, int b)
2 {
3 return a + b;
4 }
5
6 int bar(int a, int b)
7 {
8 int c = widget(a, b);
9 return c;
10 }
11
12 int foo( int a, int b)
13 {
14 int c=bar(a, b);
15 return c;
16 }
17
18 int main()
19 {
20 foo( 1, 2);
21 }
其彙編代碼
調用框架
2。 加入SEH 之後,函數調用框架稍微修改一下: 對每一個函數加入一個Exception_Registration 的鏈表,鏈表頭存放在FS:[0] 裏面。當異常拋出時,就去遍歷該鏈表找到合適的catch 塊。 對於每一個Exception_Registration 存放鏈表的上一個節點,異常處理函數( Error Handler). 用來處理異常。 這些結構都是編譯器加上的,分別在函數調用的prologue 和epilogue ,註冊和註銷 一個異常處理節點。
NOTE: error handling
1. 當異常發生時,系統得到控制權,系統從FS:[0]寄存器取到異常處理鏈的頭,以及異常的類型, 調用異常處理函數。(異常函數是編譯器生成的)
2. 從鏈表頭去匹配 異常類型和catch 塊接收的類型。( 這裏用到RTTI 信息)
3. unwind stack。這裏需要析構已經創建的對象。( 這裏需要判斷析構哪些對象,這一步是編譯器做的)
4. 執行catch 塊代碼。
後返回到程序的正常代碼,即catch塊下面的第一行代碼。
可見,在exception 找到對應的 Catche 塊後, 去棧展開(unwind stack),析構已有的對象後,進入到Catch 塊中。 問題是: 程序怎麼知道程序運行到哪裏? 哪些對象需要調用析構函數? 這也是編譯器做的,對於每一個Catch 塊,其記錄下如果該catch 塊若被調用,哪些對象需要被析構。 這有這麼一張表。具體實現可以參見reference2.
3. 當析構拋出異常時,接下來的故事。
實驗1: Base 類的析構拋出異常;
1 class Base
2 {
3 public:
4 void fun() { throw 1; }
5 ~Base() { throw 2; }
6 };
7
8 int main()
9 {
10 try
11 {
12 Base base;
13 //base.fun();
14 }
15 catch (...)
16 {
17 //cout <<"get the catch"<<endl;
18 }
19 }
運行沒有問題。
實驗2: 打開上面註釋掉的第13行代碼( //base.fun(); ),再試運行,結果呢? 在debug 模式下彈出對話框
爲什麼呢?
因爲SEH 是一個鏈表,鏈表頭地址存在FS:[0] 的寄存器裏面。 在實驗2,函數base.fun先拋出異常,從FS:[0]開始向上遍歷 SHL 節點,匹配到catch 塊。 找到代碼裏面爲一個catch塊,再去展開棧,調用base 的析構函數,然而析構又拋出異常。 如果系統再去從SEL鏈表匹配,會改變FS:[0]值,這時候程序迷失了,不知道下面該怎麼什麼? 因爲他已經丟掉了上一次異常鏈那個節點。
實驗3:如果析構函數的異常被處理呢, 程序還會正常運行嗎?
1 class Base
2 {
3 public:
4 void fun() { throw 1; }
5 ~Base()
6 {
7 try
8 {
9 throw 2;
10 }
11 catch (int e)
12 {
13 // do something
14 }
15 }
16 };
17
18 int main()
19 {
20 try
21 {
22 Base base;
23 //base.fun();
24 }
25 catch (...)
26 {
27 //cout <<"get the catch"<<endl;
28 }
29 }
的確可以運行。
因爲析構拋出來的異常,在到達上一層析構節點之前已經被別的catch 塊給處理掉。那麼當回到上一層異常函數時, 其SEH 沒有變,程序可以繼續執行。
這也許就是爲什麼C++不支持異常中拋的異常。
4. 效率:
當無異常拋出時,其開銷就是在函數調用的時候註冊/註銷 異常處理函數,這些開銷很小。
但是當異常拋出時,其開銷就大了,編譯異常鏈,用RTTI比配類型,調用析構;但是比傳統的那種返回值,層層返回,效率也不會太差。 帶來好的好處是代碼好維護,減少出錯處理的重複代碼,並且與邏輯代碼分開。
權衡一下,好處還是大大的:)
5. 總結一下流程:
爲了安全,”析構函數儘可能的不要拋出異常“。
如果非拋不可,語言也提供了方法,就是自己的異常,自己給吃掉。但是這種方法不提倡,我們提倡有錯早點報出來。
Note:
1.同樣還有一個問題,”構造函數可以拋出異常麼? 爲什麼?“
C++ 裏面當構造函數拋出異常時,其會調用構造函數裏面已經創建對象的析構函數,但是對以自己的析構函數沒有調用,就可能產生內存泄漏,比如自己new 出來的內存沒有釋放。
有兩個辦法。在Catch 塊裏面釋放已經申請的資源 或者 用智能指針把資源當做對象處理。
Delphi 裏面當構造函數拋異常時,在其執行Catch 代碼前,其先調用析構函數。
所以,構造拋出異常,是否調用析構函數,不是取決於技術,而是取決於語言的設計者。
2. 關於多線程,異常是線程安全的。 對於每一個線程都有自己的 Thread Info/Environment Block. 維護自己的SEH結構。
Reference:
1.http://www.codeproject.com/KB/cpp/exceptionhandler.aspx
2.http://baiy.cn/doc/cpp/inside_exception.htm
3.http://www.mzwu.com/article.asp?id=1469