C++異常處理解析: 異常的引起(throw), 捕獲(try catch)、異常安全

前言:

C++的異常處理機制是用於將運行時錯誤檢測和錯誤處理功能分離的一 種機制(符合高內聚低耦合的軟件工程設計要求),  這裏主要總結一下C++異常處理的基礎知識, 包括基本的如何引起異常(使用throw)和捕獲異常(try catch)相關使用注意點, 以及C++標準庫提供的一套標準異常類和這些異常類的繼承層級結構以及相關使用方法和經常使用習慣.html

C++異常的引起(throw):

引起C++異常的語法就是使用throw語句: throw object; 注意這裏throw拋出的是一個對象,也就是說是一個實例. 一旦拋出, 發生兩件事情: 第一, C++異常機制開始尋找try catch模塊, 尋找和拋出的對象的類型相匹配的catch子句找處處理代碼進行異常的處理, 這個過程是一個棧展開的 過程,也就是說C++講先從當前的函數體裏面尋找try catch模塊, 若是沒有, 則在調用當前函數(好比咱們叫當前函數A)的函數(咱們叫調用A的函數B)尋找處理代碼(在B裏面尋找), 一直尋找直到找到匹配的catch子句, 而後運行catch裏面的代碼, 運行完畢之後, 從這個匹配的catch後面的代碼繼續運行. 第二件事情是, 棧展開前面的全部函數做用域都失效(好比, A調用B, B調用C, C調用D, D調用E, E拋出異常同時在C找到了處理異常的catch子句, 那麼D, E做用域失效, 等效於D, E運行到了函數結尾), 局部對象(自動釋放內存的對象, 而不是那些動態分配內存的對象, 這一點和異常安全有關咱們後面會提到)都將調用析構函數進行銷燬.數組

注意點:安全

1. throw拋出的對象必定要是能夠複製的(C++ Primer中的原話是: 異常對象是經過複製被拋出表達式的結果建立, 該結果必須是能夠複製的類型)ide

2. 不要拋出(throw)一個數組或者函數, 緣由是, 和函數參數同樣, 數組和函數類型實參, 該實參自動轉換爲一個指針.函數

3. C++異常說明: void func(int) throw(exception type list), 代表函數func會且僅會拋出list中列舉的異常對象類型, throw()表示不會拋出任何異常(空異常類型列表)spa

C++異常的捕獲(try catch):

若是要試圖捕獲C++異常, 那麼將可能拋出(throw)異常的代碼塊放到try{}裏面, 在try{} 後面跟上catch(exception e) {}, 這裏的e是通常的異常對象, C++異常處理經過拋出對象的類型來判斷決定激活哪一個catch處理代碼. 具體語法能夠參見任何一本C++的書籍. 這裏主要提幾點注意點:設計

1. 講throw的時候也提到了, catch是一層一層catch(棧展開), 當尋找到main裏面也沒有catch捕獲的時候, C++機制通常將調用terminate終止進程(abort)指針

2.  catch子句列表中, 最特殊的catch必須最早出現, 否則永遠都不可能執行到htm

3. catch(…) 這個語法表示catch捕獲全部異常對象

4. 在catch裏面使用throw ;這條語句將從新拋出異常對象, 改異常對象是和捕獲的一場對象同一個對象(catch中能夠修改這個對象)

C++標準異常介紹(繼承層次結構等):

C++標準庫提供瞭如下的標準異常類, 他們的繼承層次結構以下(參考: Chapter 17: Advanced C++ Topics III). 比較好的寫異常的作法是繼承這些C++標準的異常類, 而後定義一組適合本身應用的異常處理對象集合. 

C++ standard exception classes inheritance diagram

C++的異常處理機制主要用於將錯誤檢測和錯誤處理功能分離, 從而達到低耦合的要求, 這篇文章主要總結了一下C++異常處理的基礎知識, 從如何使用throw引起異常, 使用try catch等捕獲異常到C++標準庫提供的一套標準異常類和這些異常類的繼承層級結構, 主要給出了相關使用方法和注意點以及一些程序設計的良好習慣. 文章全憑本人本身的理解原創行文, 若有不當之處, 在所不免, 還請不吝指正.

異常安全(內存泄露, 空指針等問題)

前言:

C++異常安全是針對C++異常處理帶來的可能的隱患(內存泄露, 空指針等)而言的, 咱們知道異常一旦發生, 程序就會轉移控制權, 若是在轉移控制權的以前, 沒有妥善處理, 好比忘記釋放內存, 空指針等, 會形成嚴重的未定義行爲或者資源泄露(內存泄露, 空指針等). 所謂異常安全, 就是爲了保證即便是發生了異常, 這些相似的未定義(內存泄露, 空指針等)行爲也不會發生.

C++異常安全概念:

咱們寫程序的時候每每習慣按照假設程序正常運行的行爲寫代碼, 管理資源等. 有時候也會寫錯誤檢測和處理的代碼, 可是在這兩個地方重疊時候, 也就是錯誤發生的時候的資源管理每每是容易被忽視的(下面立刻會給出兩個例子, 內存泄露問題和空指針未定義行爲問題).

異常安全是這麼一個概念: 這個是指, 即便發生異常, 程序也能正確操做(異常發生之後要杜絕一切未定義的行爲, 包括空指針, 內存泄露等, 即便異常發生, 那麼相關實例仍是應該保持有效的狀態).

C++異常安全要求:

C++異常安全通常有四個等級的要求(異常安全等級由低到高): 1. 沒有任何異常安全保證, 也就是異常一旦發生, 可能形成程序行爲的未定義; 2. 基本保證, 也就是異常發生的時候, 程序的行爲仍是合法的, 狀態也都是有效的, 行爲是有定義的, 可是程序實例的狀態有可能改變(仍舊合法) 3. 強保證(回滾保證), 這個等級就要求異常一旦發生而後進行處理了之後, 要麼一次性所有成功, 要麼就回滾到異常錢的原始狀態(程序狀態和異常發生之前如出一轍). 4. 保證不會有任何一方的發生.

這裏面1是最不安全的, 不可取. 4基本上等級最強, 可是通常狀況下不可能知足. 因此異常安全每每在2和3這兩個等級間取捨. 等級3有可能會有額外的負擔, 資源消耗等. 具體狀況根據程序邏輯和實際狀況判斷取捨.

C++異常安全舉例, 避免內存泄露:

C++異常安全其中一條重要的慣例, 是須要保證 若是發生異常, 被分配到的任何資源都適當地獲得釋放.  這個狀況通常發生在動態分配內存的時候, 好比我程序裏面有一段代碼, 在第20行的時候首先動態分配了內存給一個指針p, 正常運行的話, 中間有一些處理代碼, 而後到第40行delete [] p 釋放內存, 程序正常運行的話沒有問題, 可是要是在第20行到40行之間的代碼出現了異常, 程序控制權轉移給上級調用程序的時候, 這樣的代碼就有問題了, 此時, 做用域等效於已經到達了當前函數的結束, 全部局部變量或者實力都會調用自身的析構函數進行釋放資源, 可是對動態分配內存的實例來說, 由於是直接異常跳轉, 雖然做用域結束, 可是沒有執行到delete進行手動釋放, 這塊動態內存將形成內存泄露.

那麼比較好的保證這一類內存資源不泄露的異常安全的技術成爲「資源分配即初始化」(參考RAII). 對於這句話「資源分配即初始化」我本身是這麼理解的, 咱們要進行資源分配, 保證異常安全的作法不是普通的動態分配一塊內存, 而是等效的初始化一個資源管理類的實例. 這就是所謂的「資源分配即初始化」, 也就是把資源分配等效的用初始化資源管理類來替代. 那麼這裏又提到了資源管理類, 咱們解釋一下資源管理類以及「資源分配即初始化」到底好處在哪裏. 基本上這點要求咱們設計一個資源管理類統一的管理資源的分配和釋放, 更具體的, 利用構造函數分配資源, 利用析構行數釋放資源. 這樣作的好處呢, 是資源管理類自己是一個自動的局部對象, 無論是由於異常發生仍是正常的程序運行到了改局部對象的做用域的結束的時候, 這個類的析構函數都會被調用從而保證了資源的釋放, 避免了內存泄露問題. C++裏面提供了RAII的auto_ptr類, 就是一個資源管理類, 行爲雷係指針. 咱們這裏就不深刻研究它了.

C++異常安全舉例, 避免空指針:

C++異常安全的另外一個常見的管理就是須要避免空指針. 這個狀況的發生每每是咱們在動態分配內存的時候發生了異常. 好比咱們要分配p = new int[100], 這個時候要是內存不夠, 那麼就發生bad_alloc異常, p指針是空的NULL. 這個時候若是後面的代碼依賴於p的未定義行爲, 這樣很容易致使程序的崩潰. 一個有效的避免空指針的作法就是, 在賦值以前就知道內存的分配是成功仍是失敗, 一樣能夠利用咱們的資源管理類. 管理動態分配的內存, 若是分配成功, 那麼將內存塊的指針賦值給p, 若是失敗, 那麼拋出異常, 程序在p賦值前轉移了控制權,此時p的值是不會改變的. 這樣作就使得程序更加魯棒(異常發生的時候, p的狀態沒有改變, 也沒有產生未定義行爲).

錯誤處理(返回值, 錯誤標誌變量, 異常)

前言:

程序設計裏面相當重要的一塊就是錯誤處理, C++異常處理是一種面向對象的機制, 指望將錯誤處理和錯誤檢測分離. 這裏咱們結合其餘兩種錯誤處理方式(返回值, 錯誤標誌變量)來分析一下不一樣的錯誤處理(包括返回值判斷,  錯誤標誌變量, 異常處理機制)各有什麼優缺點以及各自的適用環境.

函數返回值判斷錯誤處理:

這種錯誤處理和判斷的方法基本上是使用一組錯誤處理的常量, 而後經過函數返回值, 把錯誤信息返回給函數調用者. 好比以下簡單的代碼:

const int invalidPara = -2;

const int outOfRange = -3;
 const int other = -4;
int func(int para)
{
   if(invalid parameter)
       return invalidPara;
   do something here;
   if(out of range)
       return outOfRange;
   if(other error)
       return other;
}

這樣的返回值判斷的好處在於和系統API統一, 咱們知道WinAPI以及Linux下面的系統函數都是以返回0(零)表示程序正常運行, 返回非零值表示不一樣的錯誤. 因此若是咱們也採用這樣的返回值判斷的話能夠和系統調用統一塊兒來.

可是返回值判斷錯誤的限制以及缺點也是很明顯的(我的不是很推崇用返回值, 可是也仍是要看具體狀況). 首先呢, 返回值判斷錯誤會破壞正常的返回值的做用, 使得函數調用不能被充分利用, 函數返回值不能做爲其餘表達式的組成部分, 由於這個返回值已經用來指示錯誤了而不是用來返回其餘正常的計算結果, 即便能夠既用於正常值計算又用於返回錯誤, 好比正常值都是正數, 錯誤值都是負數, 那這個結果仍是不能直接被用做任何計算, 首先仍是要判斷這個是正常計算結果呢仍是一個錯誤信息, 這就形成了計算的不方便.

其次不少時候實際上是沒辦法使用返回值來判斷錯誤信息的. 好比 1) 當func()返回類型是int的時候, 並且正常的結果的返回就是全部int型的值都有可能, 這個時候咱們其實無法找到一個很好的int value 做爲indicatro來指示這是個錯誤返回還不是一個正常的結果. 2) 編寫範型的時候好比return T, 那怎麼利用返回值來判斷? 這個時候由於咱們不明確T的類型, 因此也沒有很好的辦法利用一個明確的返回值來判斷或者給出錯誤信息. 在這些狀況下, 異常處理應該是更爲合理的錯誤處理的方式. 咱們後面第三條會再講到。接下來能夠看看第二種錯誤處理機制.

錯誤標誌變量判斷:

這個類型的錯誤判斷基本上能夠用下面的這段程序表示. 也就是設置一個錯誤標誌變量, 而後經過引用或者指針的形式傳遞給被調用的函數, 函數一旦發現錯誤就設置這個標誌, 上層調用者經過檢查這個標誌變量來判斷是否有錯誤發生.

int funcCallee(int para, int &errorFlag) {
    if(invalid parameter)
        set errorFlag and return;
    do something here;
    if(out of range)
        set errorFlag and return;
    if(other error)
        set errorFlag and return;
}

int funcCaller(int para) {
    int errorFlag = 0;
    int ret = funcCallee(1, errorFlag);
    check errorFlag;
}

這個方法的好處在於如今咱們的返回值值表示正常計算結果, 能夠被方便的利用起來, 比起第一種利用返回值判斷的話是一個比較明顯的優點, 並且前面提到的兩種不能使用返回值判斷錯誤的狀況(泛型, 正常結果返回涵蓋全部整型), 咱們也可使用標誌位. 由於表示爲老是能夠保證是int型的, 並且是不受函數的代碼邏輯影響的, 基本上是一個獨立的錯誤標誌. 在我看來這種方法彷佛並無明顯的缺陷. 我我的比較推重.

C++異常處理機制:

其實我以爲C++的異常處理就是咱們這裏說的第二種利用標誌變量的面向對象版本的錯誤處理機制, 本質上彷佛沒有太大區別. 固然異常處理還有複雜的多精細的多. 二者都是統一的獨立於程序業務邏輯的錯誤處理機制. 好比無論程序幹什麼(泛型也好, 其餘什麼也好), 咱們遇到錯誤老是可以拋出一個異常, 終止當前函數, 把控制權轉移給上層調用函數進行處理. 對應到咱們的第二種錯誤標誌變量的話, 就是檢測到異常或者錯誤的時候, 正確設置標識變量, 而後return, 控制權也轉移給上層調用函數, 上層調用函數經過判斷標誌變量的值來進行處理. 從這個角度來說, 彷佛二者也沒有太大區別.

另外一方面呢, 異常機制做爲C++的一種語言級別的機制, 其實會有比較大的開銷, 包括控制權的轉移等等, 他的好處在於錯誤處理和錯誤邏輯分離的很清楚, 並且強制使用者必定要處理異常, 不然程序將最終終止. 可是方法二呢, 要是我忘記去檢查那個錯誤標誌變量了怎麼辦? 回答是不怎麼辦. 由於這個僅僅是代碼級別的判斷, 沒有任何強制措施去要求必定要處理。 這個就是很危險的了. 因此異常機制(語言級別的判斷)從這個角度來說也是比較好的一種錯誤處理的選擇.

結束語:

這篇文章咱們仍是解析C++的異常處理機制, 這裏咱們結合其餘兩種錯誤處理方式(返回值, 錯誤標誌變量)分析了這些不一樣的錯誤處理, 即返回值判斷,  錯誤標誌變量, 異常處理機制各有什麼優缺點以及各自的適用環境.

相關文章
相關標籤/搜索