來源 https://blog.csdn.net/daheiantian/article/details/6530318 java
一句話:異常處理就是處理程序中的錯誤。python
C++之父Bjarne Stroustrup在《The C++ Programming Language》中講到:一個庫的做者能夠檢測出發生了運行時錯誤,但通常不知道怎樣去處理它們(由於和用戶具體的應用有關);另外一方面,庫的用戶知道怎樣處理這些錯誤,但卻沒法檢查它們什麼時候發生(若是能檢測,就能夠再用戶的代碼裏處理了,不用留給庫去發現)。ios
Bjarne Stroustrup說:提供異常的基本目的就是爲了處理上面的問題。基本思想是:讓一個函數在發現了本身沒法處理的錯誤時拋出(throw)一個異常,而後它的(直接或者間接)調用者可以處理這個問題。
The fundamental idea is that a function that finds a problem it cannot cope with throws an exception, hoping that its (direct or indirect) caller can handle the problem.c++
也就是《C++ primer》中說的:將問題檢測和問題處理相分離。
Exceptions let us separate problem detection from problem resolution程序員
一種思想:在全部支持異常處理的編程語言中(例如java),要認識到的一個思想:在異常處理過程當中,由問題檢測代碼能夠拋出一個對象給問題處理代碼,經過這個對象的類型和內容,實際上完成了兩個部分的通訊,通訊的內容是「出現了什麼錯誤」。固然,各類語言對異常的具體實現有着或多或少的區別,可是這個通訊的思想是不變的。編程
在C語言的世界中,對錯誤的處理老是圍繞着兩種方法:一是使用整型的返回值標識錯誤;二是使用errno宏(能夠簡單的理解爲一個全局整型變量)去記錄錯誤。固然C++中仍然是能夠用這兩種方法的。數組
這兩種方法最大的缺陷就是會出現不一致問題。例若有些函數返回1表示成功,返回0表示出錯;而有些函數返回0表示成功,返回非0表示出錯。dom
還有一個缺點就是函數的返回值只有一個,你經過函數的返回值表示錯誤代碼,那麼函數就不能返回其餘的值。固然,你也能夠經過指針或者C++的引用來返回另外的值,可是這樣可能會令你的程序略微晦澀難懂。eclipse
在若是使用異常處理的優勢有如下幾點:編程語言
1. 函數的返回值能夠忽略,但異常不可忽略。若是程序出現異常,可是沒有被捕獲,程序就會終止,這多少會促使程序員開發出來的程序更健壯一點。而若是使用C語言的error宏或者函數返回值,調用者都有可能忘記檢查,從而沒有對錯誤進行處理,結果形成程序莫名其面的終止或出現錯誤的結果。
2. 整型返回值沒有任何語義信息。而異常卻包含語義信息,有時你從類名就可以體現出來。
3. 整型返回值缺少相關的上下文信息。異常做爲一個類,能夠擁有本身的成員,這些成員就能夠傳遞足夠的信息。
4. 異常處理能夠在調用跳級。這是一個代碼編寫時的問題:假設在有多個函數的調用棧中出現了某個錯誤,使用整型返回碼要求你在每一級函數中都要進行處理。而使用異常處理的棧展開機制,只須要在一處進行處理就能夠了,不須要每級函數都處理。
任何事情都是兩面性的,異常有好處就有壞處。若是你是C++程序員,而且但願在你的代碼中使用異常,那麼下面的問題是你要注意的。
1. 性能問題。這個通常不會成爲瓶頸,可是若是你編寫的是高性能或者實時性要求比較強的軟件,就須要考慮了。
(若是你像我同樣,曾經是java程序員,那麼下面的事情可能會讓你一時迷糊,可是沒辦法,誰叫你如今學的是C++呢。)
2. 指針和動態分配致使的內存回收問題:在C++中,不會自動回收動態分配的內存,若是遇到異常就須要考慮是否正確的回收了內存。在java中,就基本不須要考慮這個,有垃圾回收機制真好!
3. 函數的異常拋出列表:java中是若是一個函數沒有在異常拋出列表中顯式指定要拋出的異常,就不容許拋出;但是在C++中是若是你沒有在函數的異常拋出列表指定要拋出的異常,意味着你能夠拋出任何異常。
4. C++中編譯時不會檢查函數的異常拋出列表。這意味着你在編寫C++程序時,若是在函數中拋出了沒有在異常拋出列表中聲明的異常,編譯時是不會報錯的。而在java中,eclipse的提示功能真的好強大啊!
5. 在java中,拋出的異常都要是一個異常類;可是在C++中,你能夠拋出任何類型,你甚至能夠拋出一個整型。(固然,在C++中若是你catch中接收時使用的是對象,而不是引用的話,那麼你拋出的對象必需要是可以複製的。這是語言的要求,不是異常處理的要求)。
6. 在C++中是沒有finally關鍵字的。而java和python中都是有finally關鍵字的。
1. 拋出和捕獲異常
很簡單,拋出異經常使用throw,捕獲用try……catch。
捕獲異常時的注意事項:
1. catch子句中的異常說明符必須是徹底類型,不能夠爲前置聲明,由於你的異常處理中經常要訪問異常類的成員。例外:只有你的catch子句使用指針或者引用接收參數,而且在catch子句內你不訪問異常類的成員,那麼你的catch子句的異常說明符才能夠是前置聲明的類型。
2. catch的匹配過程是找最早匹配的,不是最佳匹配。
3. catch的匹配過程當中,對類型的要求比較嚴格。不容許標準算術轉換和類類型的轉換。(類類型的轉化包括兩種:經過構造函數的隱式類型轉化和經過轉化操做符的類型轉化)。
4. 和函數參數相同的地方有:
① 若是catch中使用基類對象接收子類對象,那麼會形成子類對象分隔(slice)爲父類子對象(經過調用父類的複製構造函數);
② 若是catch中使用基類對象的引用接受子類對象,那麼對虛成員的訪問時,會發生動態綁定,即會多態調用。
③ 若是catch中使用基類對象的指針,那麼必定要保證throw語句也要拋出指針類型,而且該指針所指向的對象,在catch語句執行是還存在(一般是動態分配的對象指針)。
5. 和函數參數不一樣的地方有:
① 若是throw中拋出一個對象,那麼不管是catch中使用什麼接收(基類對象、引用、指針或者子類對象、引用、指針),在傳遞到catch以前,編譯器都會另外構造一個對象的副本。也就是說,若是你以一個throw語句中拋出一個對象類型,在catch處經過也是經過一個對象接收,那麼該對象經歷了兩次複製,即調用了兩次複製構造函數。一次是在throw時,將「拋出到對象」複製到一個「臨時對象」(這一步是必須的),而後是由於catch處使用對象接收,那麼須要再從「臨時對象」複製到「catch的形參變量」中; 若是你在catch中使用「引用」來接收參數,那麼不須要第二次複製,即形參的引用指向臨時變量。
② 該對象的類型與throw語句中體現的靜態類型相同。也就是說,若是你在throw語句中拋出一個指向子類對象的父類引用,那麼會發生分割現象,即只有子類對象中的父類部分會被拋出,拋出對象的類型也是父類類型。(從實現上講,是由於複製到「臨時對象」的時候,使用的是throw語句中類型的(這裏是父類的)複製構造函數)。
③ 不能夠進行標準算術轉換和類的自定義轉換:在函數參數匹配的過程當中,能夠進行不少的類型轉換。可是在異常匹配的過程當中,轉換的規則要嚴厲。
④ 異常處理機制的匹配過程是尋找最早匹配(first fit),函數調用的過程是尋找最佳匹配(best fit)。
2. 異常類型
上面已經提到過,在C++中,你能夠拋出任何類型的異常。(哎,居然能夠拋出任何類型,剛看到到這個的時候,我半天沒反應過來,由於java中這樣是不行的啊)。
注意:也是上面提到過的,在C++中若是你throw語句中拋出一個對象,那麼你拋出的對象必需要是可以複製的。由於要進行復制副本傳遞,這是語言的要求,不是異常處理的要求。(在上面「和函數參數不一樣的地方」中也講到了,由於是要複製先到一個臨時變量中)
3. 棧展開
棧展開指的是:當異常拋出後,匹配catch的過程。
拋出異常時,將暫停當前函數的執行,開始查找匹配的catch子句。沿着函數的嵌套調用鏈向上查找,直到找到一個匹配的catch子句,或者找不到匹配的catch子句。
注意事項:
1. 在棧展開期間,會銷燬局部對象。
① 若是局部對象是類對象,那麼經過調用它的析構函數銷燬。
② 可是對於經過動態分配獲得的對象,編譯器不會自動刪除,因此咱們必須手動顯式刪除。(這個問題是如此的常見和重要,以致於會用到一種叫作RAII的方法,詳情見下面講述)
2. 析構函數應該從不拋出異常。若是析構函數中須要執行可能會拋出異常的代碼,那麼就應該在析構函數內部將這個異常進行處理,而不是將異常拋出去。
緣由:在爲某個異常進行棧展開時,析構函數若是又拋出本身的未經處理的另外一個異常,將會致使調用標準庫 terminate 函數。而默認的terminate 函數將調用 abort 函數,強制從整個程序非正常退出。
3. 構造函數中能夠拋出異常。可是要注意到:若是構造函數由於異常而退出,那麼該類的析構函數就得不到執行。因此要手動銷燬在異常拋出前已經構造的部分。
4. 異常從新拋出
語法:使用一個空的throw語句。即寫成: throw;
注意問題:
① throw; 語句出現的位置,只能是catch子句中或者是catch子句調用的函數中。
② 從新拋出的是原來的異常對象,即上面提到的「臨時變量」,不是catch形參。
③ 若是但願在從新拋出以前修改異常對象,那麼應該在catch中使用引用參數。若是使用對象接收的話,那麼修改異常對象之後,不能經過「從新拋出」來傳播修改的異常對象,由於從新拋出不是catch形參,應該使用的是 throw e; 這裏「e」爲catch語句中接收的對象參數。
5. 捕獲全部異常(匹配任何異常)
語法:在catch語句中,使用三個點(…)。即寫成:catch (…) 這裏三個點是「通配符」,相似 可變長形式參數。
常見用法:與「從新拋出」表達式一塊兒使用,在catch中完成部分工做,而後從新拋出異常。
6. 未捕獲的異常
意思是說,若是程序中有拋出異常的地方,那麼就必定要對其進行捕獲處理。不然,若是程序執行過程當中拋出了一個異常,而又沒有找到相應的catch語句,那麼會和「棧展開過程當中析構函數拋出異常」同樣,會 調用terminate 函數,而默認的terminate 函數將調用 abort 函數,強制從整個程序非正常退出。
7. 構造函數的函數測試塊
對於在構造函數的初始化列表中拋出的異常,必須使用函數測試塊(function try block)來進行捕捉。語法類型下面的形式:
注意事項:在函數測試塊中捕獲的異常,在catch語句中能夠執行一個內存釋放操做,而後異常仍然會再次拋出到用戶代碼中。
8. 異常拋出列表(異常說明 exception specification)
就是在函數的形參表以後(若是是const成員函數,那麼在const以後),使用關鍵字throw聲明一個帶着括號的、可能爲空的 異常類型列表。形如:throw () 或者 throw (runtime_error, bad_alloc) 。
含義:表示該函數只能拋出 在列表中的異常類型。例如:throw() 表示不拋出任何異常。而throw (runtime_error, bad_alloc)表示只能拋出runtime_error 或bad_alloc兩種異常。
注意事項:(之前學java的尤爲要注意,和java中不太同樣)
① 若是函數沒有顯式的聲明 拋出列表,表示異常能夠拋出任意列表。(在java中,若是沒有異常拋出列表,那麼是不能拋出任何異常的)。
② C++的 「throw()」至關於java的不聲明拋出列表。都表示不拋出任何異常。
③ 在C++中,編譯的時候,編譯器不會對異常拋出列表進行檢查。也就是說,若是你聲明瞭拋出列表,即便你的函數代碼中拋出了沒有在拋出列表中指定的異常,你的程序依然能夠經過編譯,到運行時纔會出錯,對於這樣的異常,在C++中稱爲「意外異常」(unexpeced exception)。(這點和java又不相同,在java中,是要進行嚴格的檢查的)。
意外異常的處理:
若是程序中出現了意外異常,那麼程序就會調用函數unexpected()。這個函數的默認實現是調用terminate函數,即默認最終會終止程序。
虛函數重載方法時異常拋出列表的限制:
在子類中重載時,函數的異常說明 必需要比父類中要一樣嚴格,或者更嚴格。換句話說,在子類中相應函數的異常說明不能增長新的異常。或者再換句話說:父類中異常拋出列表是該虛函數的子類重載版本能夠拋出異常列表的 超集。
函數指針中異常拋出列表的限制:
異常拋出列表是函數類型的一部分,在函數指針中也能夠指定異常拋出列表。可是在函數指針初始化或者賦值時,除了要檢查返回值和形式參數外,還要注意異常拋出列表的限制:源指針的異常說明必須至少和目標指針的同樣嚴格。比較拗口,換句話說,就是聲明函數指針時指定的異常拋出列表,必定要實際函數的異常拋出列表的超集。 若是定義函數指針時不提供異常拋出列表,那麼能夠指向可以拋出任意類型異常的函數。
拋出列表是否有用:
在《More effective C++》第14條,Scott Meyers指出「要謹慎的使用異常說明」(Use exception specifications judiciously)。「異常說明」,就是咱們全部的「異常拋出列表」。之因此要謹慎,根本緣由是由於C++編譯器不會檢查異常拋出列表,這樣就可能在函數代碼中、或者調用的函數中拋出了沒有在拋出列表中指定的異常,從而致使程序調用unexpected函數,形成程序提早終止。同時他給出了三條要考慮的事情:
① 在模板中不要使用異常拋出列表。(緣由很簡單,連用來實例模板的類型都不知道,也就沒法肯定該函數是否應該拋出異常,拋出什麼異常)。
② 若是A函數內調用了B函數,而B函數沒有聲明異常拋出列表,那麼A函數自己也不該該設定異常拋出列表。(緣由是,B函數可能拋出沒有在A函數的異常拋出列表中聲明的異常,會致使調用unex函數);
③ 經過set_unexpected函數指定一個新的unexpected函數,在該函數中捕獲異常,並拋出一個統一類型的異常。
另外,在《C++ Primer》4th 中指出,雖然異常說明應用有限,可是若是可以肯定該函數不會拋出異常,那麼顯式聲明其不拋出任何異常 有好處。經過語句:"throw ()"。這樣的好處是:對於程序員,當調用這樣的函數時,不須要擔憂異常。對於編譯器,能夠執行被可能拋出異常所抑制的優化。
和java同樣,標準庫中也提供了不少的異常類,它們是經過類繼承組織起來的。標準異常被組織成八個。
異常類繼承層級結構圖以下:
每一個類所在的頭文件在圖下方標識出來.
標準異常類的成員:
① 在上述繼承體系中,每一個類都有提供了構造函數、複製構造函數、和賦值操做符重載。
② logic_error類及其子類、runtime_error類及其子類,它們的構造函數是接受一個string類型的形式參數,用於異常信息的描述;
③ 全部的異常類都有一個what()方法,返回const char* 類型(C風格字符串)的值,描述異常信息。
異常名稱 |
描述 |
exception | 全部標準異常類的父類 |
bad_alloc | 當operator new and operator new[],請求分配內存失敗時 |
bad_exception | 這是個特殊的異常,若是函數的異常拋出列表裏聲明瞭bad_exception異常,當函數內部拋出了異常拋出列表中沒有的異常,這是調用的unexpected函數中若拋出異常,不論什麼類型,都會被替換爲bad_exception類型 |
bad_typeid | 使用typeid操做符,操做一個NULL指針,而該指針是帶有虛函數的類,這時拋出bad_typeid異常 |
bad_cast | 使用dynamic_cast轉換引用失敗的時候 |
ios_base::failure | io操做過程出現錯誤 |
logic_error | 邏輯錯誤,能夠在運行前檢測的錯誤 |
runtime_error | 運行時錯誤,僅在運行時才能夠檢測的錯誤 |
logic_error的子類:
異常名稱 |
描述 |
length_error | 試圖生成一個超出該類型最大長度的對象時,例如vector的resize操做 |
domain_error | 參數的值域錯誤,主要用在數學函數中。例如使用一個負值調用只能操做非負數的函數 |
out_of_range | 超出有效範圍 |
invalid_argument | 參數不合適。在標準庫中,當利用string對象構造bitset時,而string中的字符不是’0’或’1’的時候,拋出該異常 |
runtime_error的子類:
異常名稱 |
描述 |
range_error | 計算結果超出了有意義的值域範圍 |
overflow_error | 算術計算上溢 |
underflow_error | 算術計算下溢 |
1. 爲何要編寫本身的異常類?
① 標準庫中的異常是有限的;
② 在本身的異常類中,能夠添加本身的信息。(標準庫中的異常類值容許設置一個用來描述異常的字符串)。
2. 如何編寫本身的異常類?
① 建議本身的異常類要繼承標準異常類。由於C++中能夠拋出任何類型的異常,因此咱們的異常類能夠不繼承自標準異常,可是這樣可能會致使程序混亂,尤爲是當咱們多人協同開發時。
② 當繼承標準異常類時,應該重載父類的what函數和虛析構函數。
③ 由於棧展開的過程當中,要複製異常類型,那麼要根據你在類中添加的成員考慮是否提供本身的複製構造函數。
爲何要使用類來封裝資源分配和釋放?
爲了防止內存泄露。由於在函數中發生異常,那麼對於動態分配的資源,就不會自動釋放,必需要手動顯式釋放,不然就會內存泄露。而對於類對象,會自動調用其析構函數。若是咱們在析構函數中顯式delete這些資源,就能保證這些動態分配的資源會被釋放。
如何編寫這樣的類?
將資源的分配和銷燬用類封轉起來。在析構函數中要顯式的釋放(delete或delete[])這些資源。這樣,若用戶代碼中發生異常,看成用域結束時,會調用給該類的析構函數釋放資源。這種技術被稱爲:資源分配即初始化。(resource allocation is initialization,縮寫爲"RAII")。
「用類封裝資源的分配和釋放」是如此的重要,C++標準庫爲咱們提供了一個模板類來實現這個功能。名稱爲auto_ptr,在memory頭文件中。
函數 |
功能 |
auto_ptr <T> ap() | 默認構造函數,建立名爲ap的未綁定的auto_ptr對象 |
auto_ptr<T> ap(p); | 建立名爲 ap 的 auto_ptr 對象,ap 擁有指針 p 指向的對象。該構造函數爲 explicit |
auto_ptr<T> ap1(ap2); | 建立名爲 ap1 的 auto_ptr 對象,ap1 保存原來存儲在 ap2 中的指針。將全部權轉給 ap1,ap2 成爲未綁定的 auto_ptr 對象 |
ap1 = ap2 | 將全部權 ap2 轉給 ap1。刪除 ap1 指向的對象而且使 ap1 指向 ap2 指向的對象,使 ap2 成爲未綁定的 |
~ap | 析構函數。刪除 ap 指向的對象 |
*ap | 返回對 ap 所綁定的對象的引用 |
ap-> | 返回 ap 保存的指針 |
ap.reset(p) | 若是 p 與 ap 的值不一樣,則刪除 ap 指向的對象而且將 ap 綁定到 p |
ap.release() | 返回 ap 所保存的指針而且使 ap 成爲未綁定的 |
ap.get() | 返回 ap 保存的指針 |
auto_ptr類的使用:
1. 用來保存一個指向對象類型的指針。注意必須是動態分配的對象(即便用new非配的)的指針。既不能是動態分配的數組(使用new [])指針,也不能是非動態分配的對象指針。
2. 慣用的初始化方法:在用戶代碼中,使用new表達式做爲auto_ptr構造函數的參數。(注意:auto_ptr類接受指針參數的構造函數爲explicit,因此必須顯式的進行初始化)。
3. auto_ptr的行爲特徵:相似普通指針行爲。auto_ptr存在的主要緣由就是,爲了防止動態分配的對象指針形成的內存泄露,既然是指針,其具備"*"操做符和"->"操做符。因此auto_ptr的主要目的就是:首先保證自動刪除auto_ptr所引用的對象,而且要支持普通指針行爲。
4. auto_ptr對象的複製和賦值是有破壞性的。① 會致使右操做數成爲未綁定的,致使auto_ptr對象不能放到容器中;② 在賦值的時候,將有操做符修改成未綁定,即修改了右操做數,因此要保證這裏的賦值操做符右操做數是能夠修改的左值(然而普通的賦值操做符中,右操做數能夠不是左值);③和普通的賦值操做符同樣,若是是自我賦值,那麼沒有效果;④ 致使auto_ptr對象不能放到容器中。
5. 若是auto_ptr初始化的時候,使用默認構造函數,成爲未綁定的auto_ptr對象,那麼能夠經過reset操做將其綁定到一個對象。
6. 若是但願測試auto_ptr是否已經綁定到了一個對象,那麼使用get()函數的返回值與NULL進行比較。
auto_ptr的缺陷:
1. 不能使用auto_ptr對象保存指向靜態分配的對象的指針,也不能保存指向動態分配的數組的指針。
2. 不能講兩個auto_ptr對象指向同一個對象。由於在一個auto_ptr對象析構之後,形成另外一個auto_ptr對象指向了已經釋放的內存。形成這種狀況的兩種主要常見緣由是:① 用同一個指針來初始化或者reset兩個不一樣的auto_ptr對象;② 使用一個auto_ptr對象的get函數返回值去初始化或者reset另外一個auto_ptr對象。
3. 不能將auto_ptr對象放到容器中。由於其複製和賦值操做具備破壞性。
動態內存分配錯誤
① 分配動態內存使用的是new和new[]操做符,若是他們分配內存失敗,就會拋出bad_alloc異常,在new頭文件中,因此咱們的代碼中應該捕捉這些異常。常見的代碼形式以下:
② 可使用相似C語言的方式處理,但這時要使用的nothrow版本,使用"new (nothrow)"的形式分配內存。這時,若是分配不成功,返回的是NULL指針,而再也不是拋出bad_alloc異常。
③ 能夠定製內存分配失敗行爲。C++容許指定一個new 處理程序(newhandler)回調函數。默認的並無new 處理程序,若是咱們設置了new 處理程序,那麼當new和new[] 分配內存失敗時,會調用咱們設定的new 處理程序,而不是直接拋出異常。經過set_new_handler函數來設置該回調函數。要求被回調的函數沒有返回值,也沒有形式參數。
節選自《The C++ Programming Language》 ——C++之父Bjarne Stroustrup
1. Don’t use exceptions where more local control structures will suffice; 當局部的控制可以處理時,不要使用異常;
2. Use the "resource allocation is initialization" technique to manage resources; 使用「資源分配即初始化」技術去管理資源;
3. Minimize the use of try-blocks. Use "resource acquisition is initialization" instead of explicit handler code; 儘可能少用try-catch語句塊,而是使用「資源分配即初始化」技術。
4. Throw an exception to indicate failure in a constructor; 若是構造函數內發生錯誤,經過拋出異常來指明。
5. Avoid throwing exceptions from destructors; 避免在析構函數中拋出異常。
6. Keep ordinary code and error-handling code separate; 保持普通程序代碼和異常處理代碼分開。
7. Beware of memory leaks caused by memory allocated by new not being released in case of an exception; 當心經過new分配的內存在發生異常時,可能形成內存泄露。
8. Assume that every exception that can be thrown by a function will be thrown; 若是一個函數可能拋出某種異常,那麼咱們調用它時,就要假定它必定會拋出該異常,即要進行處理。
9. Don't assume that every exception is derived from class exception; 要記住,不是全部的異常都繼承自exception類。 10. A library shouldn't unilaterally terminate a program. Instead, throw an exception and let a caller decide; 編寫的供別人調用的程序庫,不該該結束程序,而應該經過拋出異常,讓調用者決定如何處理(由於調用者必需要處理拋出的異常)。 11. Develop an error-handling strategy early in a design; 若開發一個項目,那麼在設計階段就要肯定「錯誤處理的策略」。