C++系列--異常處理

1       Exception

1.1   拋出異常

1.  異常對象的拋出與捕獲的方式與給函數傳參一樣,一個異常對象可以是能夠被傳遞給非引用參數的類型,這意味着該對象類型必須可以被拷貝。

2.  異常對象在throw對象的一個拷貝,所以這個對象類型必須是可以被拷貝的。

3. 如果異常類型有繼承關係,如:類B繼承於類A, 則:

A exc;

B *p = &exc;

throw *p;

拋出類型爲解引用時只能拋出基類B的部分,只屬於A的部分不會被拋出。

The result of dereferencing a pointer is an object whose type matches the type of the pointer.

4. 最好不要拋出一個指針,若要拋出指針,指針指向的對象必須在catch中能夠訪問,不能在catch之前被釋放, 否則會報異常。

5. 堆棧回溯(stack unwinding)

    當一個異常拋出之後,當前的函數執行被掛起,開始查找相對應的catch模塊,:

a)   首先檢查拋出異常的代碼是否在一個try之中,如果在,則查找對應的catch是否有和異常類型相匹配的,若有,則進入該catch中,若沒有,進行第二步;

b)   當前函數退出並釋放局部變量,然後檢查調用該函數的代碼是否在try中,如果在,則查找對應的catch是否有和異常類型相匹配的,若有,則進入該catch中,若沒有,繼續進行該步驟直到找到可以處理該異常類型的catch模塊,如果所有代碼都檢查完仍然沒有,則系統報錯,程序退出;

c)   若找到對應了的catch模塊,則執行進入該catch中,當catch代碼完成,則執行在該catch後繼續。

       以上這個過程就叫堆棧回溯

6. 在異常發生之前動態分配的內存如果在throw時未被釋放,會造成內存泄露,如果是局部類對象,則throw發生時,編譯器保證會自動調用其析構函數。

7. 在析構函數中不要拋出異常,因爲析構函數會在異常發生時被調用,如果在拋出異常時調用析構函數,此時析構函數中又拋出異常,這樣做通常會引起庫函數terminate被調用,程序終止。

8. 在構造函數中可以拋出異常,但必須保證異常拋出時所有分配的資源可以被釋放掉。

9. 未被處理的異常會引起程序終止,如果一個異常時未處理的,則通常會引起庫函數terminate被調用,從而程序終止。

1.2   捕獲異常

1.        catch中的異常指示符(exception specifier)指定了該catch可以處理的異常類型,該類型可以是一個內建(built-in)類型或者自定義類型,若是自定義類型,僅僅有前置聲明是不行的,必須定義

2.        catch的異常指示符可以省略參數名稱來僅僅表明此catch可以處理該類型異常,但是如果需要利用拋出的異常對象來傳遞信息,則必須定義該參數名稱。如:catch(runtime_error  &r)

3. 查找匹配的handler

    在查找匹配的catch處理時,拋出的異常對象類型並不需要完全匹配catch指示符的類型,但是隻允許以下三種情況:

n  從非const到const類型的轉換

n  從派生類類型到基類類型的轉換

n  數組類型到指針的轉換以及函數被轉換被函數指針

除了這三種情況,其他情況都不被允許,這比傳遞函數參數要嚴格的多。

4. 異常指示符(exception specifier)可以使用引用類型,C++ Primer中寫到,如果用引用效果和函數參數引用類型效果一致。但是在VC++6.0RHEL 5.4上運行結果表明,即使使用引用類型,catch異常指示符中的變量地址和拋出異常的變量地址是不一致的,所以C++ Primer中關於這點的描述有些問題

5. 捕獲異常的順序

    由於基類指示符可以捕獲子類的對象,所以如果有多個catch,並且catch中的異常指示符有繼承關係,則保持子類在前面,基類在後面,這樣可以準確地捕獲異常並處理,否則所有異常會被基類指示符的catch捕獲到。

6. 異常的再拋(rethrow)

    有些異常在一個catch中可能處理不完,需要再進行堆棧回溯讓其他catch來接着處理,則需要異常再拋(rethrow),只需要在catch中加一個throw即可。注意空的throw只能出現在catch中或者catch中調用的函數中,出現在其他地方程序會終止。

    異常再拋出的異常對象爲原始的異常,例如拋出一個子類對象,被異常指示符爲父類類型的catch捕獲到,處理後再拋拋出的仍然是子類對象,即原始的異常對象,不是父類對象。

    如果在catch中的異常指示符是一個引用,則改變了值的異常對象再拋出後會使用改變值的對象,而不是原始異常對象的值(雖然第一次catch的異常指示符爲引用,獲取的對象與原始對象不是同一個,如果在catch中改變了對象的值,則再拋時會使用改變了值的對象)。

7. 匹配所有異常的catch語法(catch-all)

    catch(…)可以捕獲所有的異常,通常可以用來做一些局部工作,然後再拋異常讓其他catch來處理。使用catch(…)可以達到釋放資源的效果,即在異常情況下可以保證動態分配的內存可以被釋放。

    catch(…)通常要放到catch列表的最後一個,否則其他所有的catch都不會被執行。

8. function try block

    在一個構造函數中,如:

    cons(char *p, int i): _ptr(p), _n(i)

    {

           ///function body

}

如果構造函數初始化列表(即_ptr與_n)被處理時是不會被函數體內的catch捕獲到,因爲構造函數初始化列表是先於構造函數內的代碼執行的,若要捕獲到這個異常,則需要將構造函數寫爲function try block。即如下形式:

cons(char *p, int i)try:_ptr(p), _n(i)

    {

           ///function body

}

catch(…)

{

    ///handler code

}

這種形式的catch既可以捕獲初始化列表的異常,又可以捕獲函數體內的異常。但是在VC++6.0這種寫法報錯

1.3   異常類層級

1. 標準異常類的繼承關係圖

      

 

       其中exception類只定義了一個虛函數what用以返回一個const char *來表明當異常被拋出時的信息。類的繼承關係越淺,類越通用,越深,定製化越嚴重,也就越不通用,比如:exception類可以用作任何的異常,而rumtime_error只用於運行時異常。

2. 自動資源釋放策略

       以下這段代碼:

       void Func()

       {

              vector<string> vec;

              string s;

              while(cin>>s)

              {

                     vec.push_back(s);

}

 

string *p = new string[p.size()];

/// snippet 1

// other code

/// snippet 2

delete []p;

}

當正常運行時,vec和p分配的資源都可以正常被釋放,但是當運行在snippet2時出現異常,釋放p的代碼就不會被運行到,但是vec會在異常處理中的堆棧回溯中調用其析構函數釋放其分配的資源,從而保證資源不會泄露。不管什麼時候vec都可以釋放其分配的資源 。

通過定義一個類來封裝資源申請和釋放的方式來保證資源可以被合理釋放,這個技術稱爲“resource allocation is initialization”,簡稱“RAII”。

在類的構造函數中申請資源,析構函數中釋放資源,這樣來保證動態資源不會泄露。如下所示:

在一個有可能出現異常的程序中應該使用類來管理資源的申請和釋放。

 

1.4   auto_ptr類

1.        auto_ptr只能用於管理new出來的單個對象,不能管理動態分配的數組,否則運行時行爲未知。

2.        auto_ptr 當被copy或者賦值時會有異常行爲,所以不要在庫容器中使用auto_ptr

3.        auto_ptr 爲異常安全的,當異常出現時會自動調用其綁定類型的析構函數。

4.        auto_ptr copy操作會轉移所包含指針對象的所有權,賦值操作會先釋放等號左邊的對象所包含的指針,在轉移所包含對象的所有權。

5.        測試auto_ptr,使用其get成員函數。

6.        可以使用初始化一個auto_ptr使用另一個auto_ptrget操作,但是這樣做會導致所包含指針被釋放兩次。

7.        auto_ptr不可以直接將一個地址直接賦值給一個auto_ptr對象,可以使用reset函數來賦值。刪除一個auto_ptr所包含的對象,可以reset(0)

 

1.5   定義異常說明

1.        void Func(int c) throw(runtime_error, logic_error); 聲明瞭一個函數,若拋出異常,會拋出runtime_error類型的異常或者其基類異常。throw後面括號爲空代表不拋出任何異常(在VC++6.0試驗,即使不遵守這個規定,也可以正常編譯和運行,第二條詳細解釋),如果函數參數列表後不指定throw,則可拋出任何類型的異常。

2.        對於違反第一條規定的函數聲明,編譯器不會報錯,只有在運行時若拋出的異常類型未被任何catch捕獲,則庫函數unexpected被調用,後者調用terminate,程序結束。

3.        定義類成員函數異常說明和普通函數一樣,但是如果有const限定符,則throwconst之後,如:int func() const throw();

4.        要保證自定義的類析構函數不拋出異常。~Destructor() throw();

5.        子類虛函數的異常說明要比基類更嚴格,不能更寬鬆,即子類拋出的異常應該父類虛函數拋出異常類型的子類或者與父類虛函數拋出的異常相同。

函數指針異常說明,對於函數指針的賦值,函數指針類型所定義的異常說明要比所賦類型函數的異常說明更加寬泛,以保證使用該函數指針的調用可以正常處理在調用該函數時產生的異常。但是如果違反這個規則,在VC++6.0VS2005都可以正常編譯通過,C++ PRIMER上寫不可以通過。

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