C++ 異常 與 ”爲什麼析構函數不能拋出異常“ 問題

轉載: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)

   程序

View Code
複製代碼
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 }
複製代碼

   其彙編代碼

View Code

   調用框架 

函數調用框架

 2。  加入SEH 之後,函數調用框架稍微修改一下: 對每一個函數加入一個Exception_Registration 的鏈表,鏈表頭存放在FS:[0] 裏面。當異常拋出時,就去遍歷該鏈表找到合適的catch 塊。 對於每一個Exception_Registration 存放鏈表的上一個節點,異常處理函數( Error Handler). 用來處理異常。 這些結構都是編譯器加上的,分別在函數調用的prologue 和epilogue ,註冊和註銷 一個異常處理節點。

加入SEH 後的函數調用框架

 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

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