前言:
C++的異常處理機制是用於將運行時錯誤檢測和錯誤處理功能分離的一 種機制(符合高內聚低耦合的軟件工程設計要求), 這里主要總結一下C++異常處理的基礎知識, 包括基本的如何引發異常(使用throw)和捕獲異常(try catch)相關使用注意點, 以及C++標准庫提供的一套標准異常類和這些異常類的繼承層級結構以及相關使用方法和常用習慣.
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中的原話是: 異常對象是通過復制被拋出表達式的結果創建, 該結果必須是可以復制的類型)
2. 不要拋出(throw)一個數組或者函數, 原因是, 和函數參數一樣, 數組和函數類型實參, 該實參自動轉換為一個指針.
3. C++異常說明: void func(int) throw(exception type list), 表明函數func會且僅會拋出list中列舉的異常對象類型, throw()表示不會拋出任何異常(空異常類型列表)
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必須最先出現, 不然永遠都不可能執行到
3. catch(…) 這個語法表示catch捕獲所有異常
4. 在catch里面使用throw ;這條語句將重新拋出異常對象, 改異常對象是和捕獲的一場對象同一個對象(catch中可以修改這個對象)
C++標准異常介紹(繼承層次結構等):
C++標准庫提供了以下的標准異常類, 他們的繼承層次結構如下(參考: Chapter 17: Advanced C++ Topics III). 比較好的寫異常的做法是繼承這些C++標准的異常類, 然后定義一組適合自己應用的異常處理對象集合.

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++的異常處理機制, 這里我們結合其他兩種錯誤處理方式(返回值, 錯誤標志變量)分析了這些不同的錯誤處理, 即返回值判斷, 錯誤標志變量, 異常處理機制各有什么優缺點以及各自的適用環境.
