C++ 異常處理

前言:
異常處理是C++的一項語言機制,用於在程序中處理異常事件。異常事件在C++中表示爲異常對象。異常事件發生時,程序使用throw關鍵字拋出異常表達式,拋出點稱爲異常出現點,由操作系統爲程序設置當前異常對象,然後執行程序的當前異常處理代碼塊,在包含了異常出現點的最內層的try塊,依次匹配catch語句中的異常對象(只進行類型匹配,catch參數有時在catch語句中並不會使用到)。若匹配成功,則執行catch塊內的異常處理語句,然後接着執行try...catch...塊之後的代碼。如果在當前的try...catch...塊內找不到匹配該異常對象的catch語句,則由更外層的try...catch...塊來處理該異常;如果當前函數內所有的try...catch...塊都不能匹配該異常,則遞歸回退到調用棧的上一層去處理該異常。如果一直退到主函數main()都不能處理該異常,則調用系統函數terminate()終止程序。

一:C/C++中的異常處理
增強錯誤恢復能力是提高代碼健壯性的最有力的途徑之一,C語言中採用的錯誤處理方法被認爲是緊耦合的,函數的使用者必須在非常靠近函數調用的地方編寫錯誤處理代碼,這樣會使得其變得笨拙和難以使用。C++中引入了異常處理機制,這是C++的主要特徵之一,是考慮問題和處理錯誤的一種更好的方式。使用錯誤處理可以帶來一些優點,如下:
·        錯誤處理代碼的編寫不再冗長乏味,並且不再和正常的代碼混合在一起,程序員只需要編寫希望產生的代碼,然後在後面某個單獨的區段裏編寫處理錯誤的嗲嗎。多次調用同一個函數,則只需要某個地方編寫一次錯誤處理代碼。
·        錯誤不能被忽略,如果一個函數必須向調用者發送一次錯誤信息。它將拋出一個描述這個錯誤的對象。
傳統的錯誤處理和異常處理
在討論異常處理之前,我們先談談C語言中的傳統錯誤處理方法,這裏列舉了如下三種:
·        在函數中返回錯誤,函數會設置一個全局的錯誤狀態標誌。
·        使用信號來做信號處理系統,在函數中raise信號,通過signal來設置信號處理函數,這種方式耦合度非常高,而且不同的庫產生的信號值可能會發生衝突
·        使用標準C庫中的非局部跳轉函數 setjmp和longjmp


(1) setjmp和longjmp————C 語言中的長跳轉
非局部跳轉語句---setjmp和longjmp函數。非局部指的是,這不是由普通C語言goto,語句在一個函數內實施的跳轉,而是在棧上跳過若干調用幀,返回到當前函數調用路徑上的某一個函數中。
#include <setjmp.h>
Int setjmp(jmp_buf  env);
       返回值:若直接調用則返回0,若從longjmp調用返回則返回非0值的longjmp中的val值
Void longjmp(jmp_buf env,int val);
        調用此函數則返回到語句setjmp所在的地方,其中env 就是setjmp中的 env,而val 則是使setjmp的返回值變爲val。
        當檢查到一個錯誤時,則以兩個參數調用longjmp函數,第一個就是在調用setjmp時所用的env,第二個參數是具有非0值的val,它將成爲從setjmp處返回的值。使用第二個參數的原因是對於一個setjmp可以有多個longjmp。
 #include <iostream> 
 #include <setjmp.h> 
 jmp_buf static_buf; //用來存放處理器上下文,用於跳轉 
 
void do_jmp() 

    //do something,simetime occurs a little error 
        //調用longjmp後,會載入static_buf的處理器信息,然後第二個參數作爲返回點的setjmp這個函數的返回值 
         longjmp(static_buf,10);//10是錯誤碼,根據這個錯誤碼來進行相應的處理 
   } 
    
   int main() 
   { 
       int ret = 0; 
       //將處理器信息保存到static_buf中,並返回0,相當於在這裏做了一個標記,後面可以跳轉過來 
      if((ret = setjmp(static_buf)) == 0) { 
           //要執行的代碼 
           do_jmp(); 
       } else {    //出現了錯誤 
           if (ret == 10) 
               std::cout << "a little error" << std::endl; 
      } 
  } 
錯誤處理方式看起來耦合度不是很高,正常代碼和錯誤處理的代碼分離了,處理處理的代碼都匯聚在一起了。但是基於這種局部跳轉的方式來處理代碼,在 C++中卻存在很嚴重的問題,那就是對象不能被析構,局部跳轉後不會主動去調用已經實例化對象的析構函數。這將導致內存泄露的問題
(2)C++ 中的異常處理
異常處理是C++的一項語言機制,用於在程序中處理異常事件。異常事件在C++中表示爲異常對象。異常事件發生時,程序使用throw關鍵字拋出異常表達式,拋出點稱爲異常出現點,由操作系統爲程序設置當前異常對象,然後執行程序的當前異常處理代碼塊,在包含了異常出現點的最內層的try塊,依次匹配catch語句中的異常對象(只進行類型匹配,catch參數有時在catch語句中並不會使用到)。若匹配成功,則執行catch塊內的異常處理語句,然後接着執行try...catch...塊之後的代碼。如果在當前的try...catch...塊內找不到匹配該異常對象的catch語句,則由更外層的try...catch...塊來處理該異常;如果當前函數內所有的try...catch...塊都不能匹配該異常,則遞歸回退到調用棧的上一層去處理該異常。如果一直退到主函數main()都不能處理該異常,則調用系統函數terminate()終止程序。

//示例代碼:throw包含在外層函數的try塊中
void registerScore(int score)
{
    if (score > 100 || score < 0)
        throw score; //throw語句被包含在外層main的try語句塊中
    //將分數寫入文件或進行其他操作
}
int main()
{
    int score=0;
    while (cin >> score)
    {
        try
        {
            registerScore(score);
        }
        catch (int score)
        {
            cerr << "你輸入的分數數值有問題,請重新輸入!";
            continue;
        }
    }
}

二:C++中的異常類
異常對象是一種特殊的對象,編譯器依據異常拋出表達式複製構造異常對象,這要求拋出異常表達式不能是一個不完全類型(一個類型在聲明之後定義之前爲一個不完全類型。不完全類型意味着該類型沒有完整的數據與操作描述),而且可以進行復制構造,這就要求異常拋出表達式的複製構造函數(或移動構造函數)、析構函數不能是私有的。
異常對象不同於函數的局部對象,局部對象在函數調用結束後就被自動銷燬,而異常對象將駐留在所有可能被激活的catch語句都能訪問到的內存空間中,也即上文所說的TIB。當異常對象與catch語句成功匹配上後,在該catch語句的結束處被自動析構。
在函數中返回局部變量的引用或指針幾乎肯定會造成錯誤,同樣的道理,在throw語句中拋出局部變量的指針或引用也幾乎是錯誤的行爲。如果指針所指向的變量在執行catch語句時已經被銷燬,對指針進行解引用將發生意想不到的後果。
throw出一個表達式時,該表達式的靜態編譯類型將決定異常對象的類型。所以當throw出的是基類指針的解引用,而該指針所指向的實際對象是派生類對象,此時將發生派生類對象切割。
除了拋出用戶自定義的類型外,C++標準庫定義了一組類,用戶報告標準庫函數遇到的問題。這些標準庫異常類只定義了幾種運算,包括創建或拷貝異常類型對象,以及爲異常類型的對象賦值。
三:棧展開、RAII
其實棧展開已經在前面說過,就是從異常拋出點一路向外層函數尋找匹配的catch語句的過程,尋找結束於某個匹配的catch語句或標準庫函數terminate。這裏重點要說的是棧展開過程中對局部變量的銷燬問題。我們知道,在函數調用結束時,函數的局部變量會被系統自動銷燬,類似的,throw可能會導致調用鏈上的語句塊提前退出,此時,語句塊中的局部變量將按照構成生成順序的逆序,依次調用析構函數進行對象的銷燬
但是這其中有一個隱含的問題:
int main()
{
    try
    {
        A * a= new A;
        throw *a;
    }
    catch (A a)
    {
        ;
    }
    getchar();
    return 0;
}
只是執行了兩次析構函數,說明發生了內存泄露,RAII機制有助於解決這個問題,RAII(Resource acquisition is initialization,資源獲取即初始化)。它的思想是以對象管理資源。爲了更爲方便、魯棒地釋放已獲取的資源,避免資源死鎖,一個辦法是把資源數據用對象封裝起來。程序發生異常,執行棧展開時,封裝了資源的對象會被自動調用其析構函數以釋放資源。C++中的智能指針便符合RAII。關於這個問題詳細可以看《Effective C++》條款13.
四:異常機制與構造函數
異常機制的一個合理的使用是在構造函數中。構造函數沒有返回值,所以應該使用異常機制來報告發生的問題。更重要的是,構造函數拋出異常表明構造函數還沒有執行完,其對應的析構函數不會自動被調用,因此析構函數應該先析構所有所有已初始化的基對象,成員對象,再拋出異常。
C++類構造函數初始化列表的異常機制,稱爲function-try block。一般形式爲:
myClass::myClass(type1 pa1)
    try:  _myClass_val (初始化值)
{
  /*構造函數的函數體 */
}
  catch ( exception& err )
{
  /* 構造函數的異常處理部分 */
};
五:異常機制與析構函數
C++不禁止析構函數向外界拋出異常,但析構函數被期望不向外界函數拋出異常。析構函數中向函數外拋出異常,將直接調用terminator()系統函數終止程序。如果一個析構函數內部拋出了異常,就應該在析構函數的內部捕獲並處理該異常,不能讓異常被拋出析構函數之外。可以如此處理:
若析構函數拋出異常,調用std::abort()來終止程序。
在析構函數中catch捕獲異常並作處理。
關於具體細節,有興趣可以看《Effective C++》條款08:別讓異常逃離析構函數。

六:noexcept修飾符與noexcept操作符
noexcept修飾符是C++11新提供的異常說明符,用於聲明一個函數不會拋出異常。編譯器能夠針對不拋出異常的函數進行優化,另一個顯而易見的好處是你明確了某個函數不會拋出異常,別人調用你的函數時就知道不用針對這個函數進行異常捕獲。在C++98中關於異常處理的程序中你可能會看到這樣的代碼:
void func() throw(int ,double ) {...}
void func() throw(){...}
這是throw作爲函數異常說明,前者表示func()這個函數可能會拋出int或double類型的異常,後者表示func()函數不會拋出異常。事實上前者很少被使用,在C++11這種做法已經被摒棄,而後者則被C++11的noexcept異常聲明所代替:
void func() noexcept {...}
//等價於void func() throw(){...}
在C++11中,編譯器並不會在編譯期檢查函數的noexcept聲明,因此,被聲明爲noexcept的函數若攜帶異常拋出語句還是可以通過編譯的。在函數運行時若拋出了異常,編譯器可以選擇直接調用terminate()函數來終結程序的運行,因此,noexcept的一個作用是阻止異常的傳播,提高安全性.
上面一點提到了,我們不能讓異常逃出析構函數,因爲那將導致程序的不明確行爲或直接終止程序。實際上出於安全的考慮,C++11標準中讓類的析構函數默認也是noexcept的。 同樣是爲了安全性的考慮,經常被析構函數用於釋放資源的delete函數,C++11也默認將其設置爲noexcept。
noexcept也可以接受一個常量表達式作爲參數,例如:
void func() noexcept(常量表達式);
常量表達式的結果會被轉換成bool類型,noexcept(bool)表示函數不會拋出異常,noexcept(false)則表示函數有可能會拋出異常。故若你想更改析構函數默認的noexcept聲明,可以顯式地加上noexcept(false)聲明,但這並不會帶給你什麼好處。
七:異常處理的性能分析
異常處理機制的主要環節是運行期類型檢查。當拋出一個異常時,必須確定異常是不是從try塊中拋出。異常處理機制爲了完善異常和它的處理器之間的匹配,需要存儲每個異常對象的類型信息以及catch語句的額外信息。由於異常對象可以是任何類型(如用戶自定義類型),並且也可以是多態的,獲取其動態類型必須要使用運行時類型檢查(RTTI),此外還需要運行期代碼信息和關於每個函數的結構。
當異常拋出點所在函數無法解決異常時,異常對象沿着調用鏈被傳遞出去,程序的控制權也發生了轉移。轉移的過程中爲了將異常對象的信息攜帶到程序執行處(如對異常對象的複製構造或者catch參數的析構),在時間和空間上都要付出一定的代價,本身也有不安全性,特別是異常對象是個複雜的類的時候。
異常處理技術在不同平臺以及編譯器下的實現方式都不同,但都會給程序增加額外的負擔,當異常處理被關閉時,額外的數據結構、查找表、一些附加的代碼都不會被生成,正是因爲如此,對於明確不拋出異常的函數,我們需要使用noexcept進行聲明。
發佈了35 篇原創文章 · 獲贊 5 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章