什么是異常?
????????在程序運行的過程中,我們不可能保證我們的程序百分百不出現異常和錯誤,那么出現異常時該怎么報錯,讓我們知道是哪個地方錯誤了呢?
????????C++中就提供了異常處理的機制。
一、異常處理的關鍵字
(1)throw: 當問題出現時,程序會拋出一個異常。這是通過使用 throw 關鍵字來完成的。
(2)catch: 在您想要處理問題的地方,通過異常處理程序捕獲異常。catch 關鍵字用于捕獲異常。
(3)try:try 塊中的代碼標識將被激活的特定異常。它后面通常跟著一個或多個 catch 塊(至少要跟一個)。
????????當被try的括號包圍的代碼塊(我們可稱之為保護代碼)出現異常時,就會拋出異常,catch塊的代碼就會捕獲異常并執行catch包圍的代碼塊。因為異常也可以有不同的種類,所以我們可以在try后面跟著多個catch塊,捕獲不同的異常,如下:
try
{// 保護代碼
}
catch( ExceptionName1 e1 )//第一種異常
{// catch 塊
}
catch( ExceptionName2 e2 )//第二種異常
{// catch 塊
}
catch( ExceptionName3 eN )//第三種異常
{// catch 塊
}
二、捕獲異常
????????使用 throw 語句可以在代碼塊中的任何地方拋出異常。
????????throw 語句的操作數可以是任意的表達式,表達式的結果的類型決定了拋出的異常的類型。
如下:
????????可以看到,我們拋出的是一串字符串,所以編譯器報錯的信息說異常類型是char(其實是const char*,但它簡寫了)。
????????實際我們編程的時候,我們肯定不希望編譯器就只給我說異常類型是char*就完了。我們還希望把它打印出來,讓我們看得到"出現了除數為0的錯誤",這就需要捕獲異常。
#include <iostream>
using namespace std;
double division(double a, double b)
{try{if (b == 0){throw "出現了除數為0的錯誤";//throw關鍵字拋出錯誤類型 const char*}}catch (const char* error_string)//捕獲const char*類型的錯誤{cout << error_string << endl;//打印它}return (a / b);
}
int main()
{cout << division(1, 0);
}
????????可見,有了try和catch后,程序不會在遇到錯誤時崩潰終止,而是會處理它,然后繼續運行。
所以這個程序依然能輸出計算結果inf(無限大),但是在此之前也打印了報錯信息。
總之,利用異常處理機制,我們就可以在程序運行時處理異常,大大減少程序崩潰的概率。
你在生活中經常遇到軟件閃退的問題,十有八九沒有做好異常處理。
三、C++標準庫自帶的異常類型
下表是對上面層次結構中出現的每個異常的說明:
std::exception 該異常是所有標準 C++ 異常的父類。 std::bad_alloc 該異常可以通過 new 拋出。
std::bad_cast 該異常可以通過 dynamic_cast 拋出。 std::bad_typeid 該異常可以通過 typeid
拋出。 std::bad_exception 這在處理 C++ 程序中無法預期的異常時非常有用。
std::logic_error 理論上可以通過讀取代碼來檢測到的異常。
std::domain_error 當使用了一個無效的數學域時,會拋出該異常。
std::invalid_argument 當使用了無效的參數時,會拋出該異常。
std::length_error 當創建了太長的std::string 時,會拋出該異常。
std::out_of_range 該異常可以通過方法拋出,例如 std::vector 和std::bitset<>::operator。
std::runtime_error 理論上不可以通過讀取代碼來檢測到的異常。
std::overflow_error 當發生數學上溢時,會拋出該異常。
std::range_error 當嘗試存儲超出范圍的值時,會拋出該異常。
std::underflow_error 當發生數學下溢時,會拋出該異常。
四、異常規格說明
異常規格說明的目的是為了讓函數使用者知道該函數可能拋出那些異常,可以再函數的聲明中列出這個函數可能拋出的所有異常類型,例如:
viod fun() throw(A,B,C,D);
????????throw后面的括號內,必須寫出會拋出的異常的類型,如果括號里面是空的,說明這個函數不會拋出任何異常。
????????如果你不聲明throw(A,B,C,D),那么編譯器就會認為這個函數什么異常都可能會拋出。
????????異常規范聲明可以讓編譯器知道可能會拋出的異常有哪些,就可以提升編譯速度和給編譯器更多的優化空間,讓你的代碼性能更高,也可以讓其他程序員了解你這段代碼可能會產生的異常,提高代碼可讀性。
五、noexcept關鍵字
在C++11中新增了noexcept關鍵字以表示這個函數不會拋出某種異常。并且可以阻止異常的傳播。
無條件的noexcept關鍵字
當我們聲明的noexcept關鍵字無條件時,表示這個函數中所的所有代碼都不會產生異常,如下:
void fun() noexcept; //C++11
void fun() noexcept(); //也可以寫成這樣,等價的
void fun() noexcept(...); //也可以寫成這樣,等價的
void fun() noexcept(true); //也可以寫成這樣,等價的
有條件的noexcept關鍵字
void fun(Type& x, Type& y) noexcept(noexcept(noexcept(fun1()),noexcept(fun2()));
//表示fun函數內會調用到的fun1函數和fun2函數都不會拋出異常,但不保證其他代碼不會拋出異常
什么時候我們需要noexcept關鍵字?
????????使用noexcept表明函數或操作不會發生異常,會給編譯器更大的優化空間。然而,并不是加上noexcept就能提高效率。
以下情形鼓勵使用noexcept:
(1)移動構造函數(move constructor)
(2)移動分配函數(move assignment)
(3)析構函數(destructor)。這里提一句,在新版本的編譯器中,析構函數是默認加上關鍵字noexcept的。下面代碼可以檢測編譯器是否給析構函數加上關鍵字noexcept。
(4)葉子函數(Leaf Function)。葉子函數是指在函數內部不分配棧空間,也不調用其它函數,也不存儲非易失性寄存器,也不處理異常。
最后強調一句,在不是以上情況或者沒把握的情況下,不要輕易使用noexcept。
六、定義新的異常類型
只有C++標準庫自帶的異常類型肯定是不夠用的,我們實際工作中還需要根據項目需求定義新的異常類型。
#include <iostream>
#include <exception>
using namespace std;
class DivisionZeroException :public exception//基于exception類定義新的異常類型除以0導致的異常類
{
public://這里重載了父類的虛函數what()//throw ()的括號里面沒有東西,這表示這個函數不會拋出任何異常//const 是常量的關鍵字,常量在定義后無法被修改const char* what() const throw (){return "出現了除以0的錯誤\n";}
};
double division(double a, double b)
{try{if (b == 0){DivisionZeroException e;throw e;//throw關鍵字拋出錯誤類型DivisionZeroException}}catch (DivisionZeroException e)//捕獲const char*類型的錯誤{cout<<e.what();//打印什么異常了}return (a / b);
}
int main()
{cout << division(1, 0);
}
出現了除數為0的錯誤
inf