第七章 C++異常
7.1 異沉裕基本概念
Bjarne Stroustrup說:提供異常的基本目的就是為了處理上面的問題∫嘉粒基本思想是:讓一個函數在發(fā)現(xiàn)了自己無法處理的錯誤時拋出(throw
)一個異常谆扎,然后它的(直接或者間接)調用者能夠處理這個問題镰吆。也就是《C++ primer》中說的:將問題檢測和問題處理相分離。
一種思想:在所有支持異常處理的編程語言中(例如java
)洪囤,要認識到的一個思想:在異常處理過程中徒坡,由問題檢測代碼可以拋出一個對象給問題處理代碼,通過這個對象的類型和內容瘤缩,實際上完成了兩個部分的通信喇完,通信的內容是“出現(xiàn)了什么錯誤”。當然剥啤,各種語言對異常的具體實現(xiàn)有著或多或少的區(qū)別锦溪,但是這個通信的思想是不變的不脯。
一句話:異常處理就是處理程序中的錯誤。所謂錯誤是指在程序運行的過程中發(fā)生的一些異常事件(如:除0溢出刻诊,數組下標越界防楷,所要讀取的文件不存在,空指針,內存不足等等)则涯。
回顧一下:我們以前編寫程序是如何處理異常复局?
在C語言的世界中,對錯誤的處理總是圍繞著兩種方法:一是使用整型的返回值標識錯誤粟判;二是使用errno宏(可以簡單的理解為一個全局整型變量)去記錄錯誤亿昏。當然C++中仍然是可以用這兩種方法的。
這兩種方法最大的缺陷就是會出現(xiàn)不一致問題档礁。例如有些函數返回1表示成功角钩,返回0表示出錯;而有些函數返回0表示成功呻澜,返回非0表示出錯递礼。
還有一個缺點就是函數的返回值只有一個,你通過函數的返回值表示錯誤代碼易迹,那么函數就不能返回其他的值宰衙。當然,你也可以通過指針或者C++的引用來返回另外的值睹欲,但是這樣可能會令你的程序略微晦澀難懂供炼。
c++異常機制相比C語言異常處理的優(yōu)勢?
- 函數的返回值可以忽略,但異常不可忽略窘疮。如果程序出現(xiàn)異常袋哼,但是沒有被捕獲,程序就會終止闸衫,這多少會促使程序員開發(fā)出來的程序更健壯一點涛贯。而如果使用C語言的
error
宏或者函數返回值,調用者都有可能忘記檢查蔚出,從而沒有對錯誤進行處理弟翘,結果造成程序莫名其面的終止或出現(xiàn)錯誤的結果。 - 整型返回值沒有任何語義信息骄酗。而異常卻包含語義信息稀余,有時你從類名就能夠體現(xiàn)出來。
- 整型返回值缺乏相關的上下文信息趋翻。異常作為一個類睛琳,可以擁有自己的成員,這些成員就可以傳遞足夠的信息。
- 異常處理可以在調用跳級师骗。這是一個代碼編寫時的問題:假設在有多個函數的調用棧中出現(xiàn)了某個錯誤历等,使用整型返回碼要求你在每一級函數中都要進行處理。而使用異常處理的棧展開機制辟癌,只需要在一處進行處理就可以了寒屯,不需要每級函數都處理。
//如果判斷返回值愿待,那么返回值是錯誤碼還是結果浩螺?
//如果不判斷返回值,那么b==0時候仍侥,程序結果已經不正確
//A寫的代碼
int A_MyDivide(int a,int b){
if (b == 0){
return -1;
}
return a / b;
}
//B寫的代碼
int B_MyDivide(int a,int b){
int ba = a + 100;
int bb = b;
int ret = A_MyDivide(ba, bb); //由于B沒有處理異常要出,導致B結果運算錯誤
return ret;
}
//C寫的代碼
int C_MyDivide(){
int a = 10;
int b = 0;
int ret = B_MyDivide(a, b); //更嚴重的是,由于B沒有繼續(xù)拋出異常农渊,導致C的代碼沒有辦法捕獲異常
if (ret == -1){
return -1;
}
else{
return ret;
}
}
//所以,我們希望:
//1.異常應該捕獲患蹂,如果你捕獲,可以砸紊,那么異常必須繼續(xù)拋給上層函數,你不處理传于,不代表你的上層不處理
//2.這個例子,異常沒有捕獲的結果就是運行結果錯的一塌糊涂醉顽,結果未知沼溜,未知的結果程序沒有必要執(zhí)行下去
7.2 異常語法
7.2.1 異常基本語法
int A_MyDivide(int a, int b){
if (b == 0){
throw 0;
}
return a / b;
}
//B寫的代碼 B寫代碼比較粗心游添,忘記處理異常
int B_MyDivide(int a, int b){
int ba = a;
int bb = b;
int ret = A_MyDivide(ba, bb) + 100; //由于B沒有處理異常系草,導致B結果運算錯誤
return ret;
}
//C寫的代碼
int C_MyDivide(){
int a = 10;
int b = 0;
int ret = 0;
//沒有處理異常,程序直接中斷執(zhí)行
#if 1
ret = B_MyDivide(a, b);
//處理異常
#else
try{
ret = B_MyDivide(a, b); //更嚴重的是唆涝,由于B沒有繼續(xù)拋出異常找都,導致C的代碼沒有辦法捕獲異常
}
catch (int e){
cout << "C_MyDivide Call B_MyDivide 除數為:" << e << endl;
}
#endif
return ret;
}
int main(){
C_MyDivide();
system("pause");
return EXIT_SUCCESS;
}
總結:
- 若有異常則通過
throw
操作創(chuàng)建一個異常對象并拋出。 - 將可能拋出異常的程序段放到
try
塊之中廊酣。 - 如果在
try
段執(zhí)行期間沒有引起異常能耻,那么跟在try
后面的catch
字句就不會執(zhí)行。 -
catch
子句會根據出現(xiàn)的先后順序被檢查亡驰,匹配的catch
語句捕獲并處理異常(或繼續(xù)拋出異常) - 如果匹配的處理未找到晓猛,則運行函數
terminate
將自動被調用,其缺省功能調用abort終止程序凡辱。 - 處理不了的異常鞍帝,可以在
catch
的最后一個分支,使用throw
煞茫,向上拋。
c++異常處理使得異常的引發(fā)和異常的處理不必在一個函數中,這樣底層的函數可以著重解決具體問題续徽,而不必過多的考慮異常的處理蚓曼。上層調用者可以在適當的位置設計對不同類型異常的處理。
7.2.2 異常嚴格類型匹配
異常機制和函數機制互不干涉,但是捕捉方式是通過嚴格類型匹配钦扭。
void TestFunction(){
cout << "開始拋出異常..." << endl;
//throw 10; //拋出int類型異常
//throw 'a'; //拋出char類型異常
//throw "abcd"; //拋出char*類型異常
string ex = "string exception!";
throw ex;
}
int main(){
try{
TestFunction();
}
catch (int){
cout << "拋出Int類型異常!" << endl;
}
catch (char){
cout << "拋出Char類型異常!" << endl;
}
catch (char*){
cout << "拋出Char*類型異常!" << endl;
}
catch (string){
cout << "拋出string類型異常!" << endl;
}
//捕獲所有異常
catch (...){
cout << "拋出其他類型異常!" << endl;
}
system("pause");
return EXIT_SUCCESS;
}
7.2.3 棧解旋(unwinding)
異常被拋出后纫版,從進入try
塊起,到異常被拋擲前客情,這期間在棧上構造的所有對象其弊,都會被自動析構。析構的順序與構造的順序相反膀斋,這一過程稱為棧的解旋(unwinding).
class Person{
public:
Person(string name){
mName = name;
cout << mName << "對象被創(chuàng)建!" << endl;
}
~Person(){
cout << mName << "對象被析構!" << endl;
}
public:
string mName;
};
void TestFunction(){
Person p1("aaa");
Person p2("bbb");
Person p3("ccc");
//拋出異常
throw 10;
}
int main(){
try{
TestFunction();
}
catch (...){
cout << "異常被捕獲!" << endl;
}
system("pause");
return EXIT_SUCCESS;
}
7.2.4 異常接口聲明
- 為了加強程序的可讀性梭伐,可以在函數聲明中列出可能拋出異常的所有類型,例如:
void func() throw(A,B,C);
這個函數func
能夠且只能拋出類型A,B,C及其子類型的異常仰担。 - 如果在函數聲明中沒有包含異常接口聲明糊识,則此函數可以拋任何類型的異常,例如:
void func()
- 一個不拋任何類型異常的函數可聲明為:
void func() throw()
- 如果一個函數拋出了它的異常接口聲明所不允許拋出的異常,
unexcepted
函數會被調用摔蓝,該函數默認行為調用terminate
函數中斷程序赂苗。
//可拋出所有類型異常
void TestFunction01(){
throw 10;
}
//只能拋出int char char*類型異常
void TestFunction02() throw(int,char,char*){
string exception = "error!";
throw exception;
}
//不能拋出任何類型異常
void TestFunction03() throw(){
throw 10;
}
int main(){
try{
//TestFunction01();
//TestFunction02();
//TestFunction03();
}
catch (...){
cout << "捕獲異常!" << endl;
}
system("pause");
return EXIT_SUCCESS;
}
請分別在qt vs linux
下做測試! Qt and Linux
正確!
7.2.5 異常變量生命周期
-
throw
的異常是有類型的,可以是數字贮尉、字符串拌滋、類對象。 -
throw
的異常是有類型的猜谚,catch
需嚴格匹配異常類型败砂。
class MyException
{
public:
MyException(){
cout << "異常變量構造" << endl;
};
MyException(const MyException & e)
{
cout << "拷貝構造" << endl;
}
~MyException()
{
cout << "異常變量析構" << endl;
}
};
void DoWork()
{
throw new MyException(); //test1 2都用 throw MyExecption();
}
void test01()
{
try
{
DoWork();
}
catch (MyException e)
{
cout << "捕獲 異常" << endl;
}
}
void test02()
{
try
{
DoWork();
}
catch (MyException &e)
{
cout << "捕獲 異常" << endl;
}
}
void test03()
{
try
{
DoWork();
}
catch (MyException *e)
{
cout << "捕獲 異常" << endl;
delete e;
}
}
7.2.6 異常的多態(tài)使用
//異常基類
class BaseException{
public:
virtual void printError(){};
};
//空指針異常
class NullPointerException : public BaseException{
public:
virtual void printError(){
cout << "空指針異常!" << endl;
}
};
//越界異常
class OutOfRangeException : public BaseException{
public:
virtual void printError(){
cout << "越界異常!" << endl;
}
};
void doWork(){
throw NullPointerException();
}
void test()
{
try{
doWork();
}
catch (BaseException& ex){
ex.printError();
}
}
7.3 C++標準異常庫
7.3.1 標準庫介紹
標準庫中也提供了很多的異常類龄毡,它們是通過類繼承組織起來的吠卷。異常類繼承層級結構圖如下:
每個類所在的頭文件在圖下方標識出來。
標準異常類的成員:
- ① 在上述繼承體系中沦零,每個類都有提供了構造函數祭隔、復制構造函數、和賦值操作符重載路操。
- ②
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操作過程出現(xiàn)錯誤 |
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 | 算術計算下溢 |
invalid_argument | 參數不合適吴攒。在標準庫中张抄,當利用string對象構造bitset時,而string中的字符不是’0’或’1’的時候洼怔,拋出該異常 |
#include<stdexcept>
class Person{
public:
Person(int age){
if (age < 0 || age > 150){
throw out_of_range("年齡應該在0-150歲之間!");
}
}
public:
int mAge;
};
int main(){
try{
Person p(151);
}
catch (out_of_range& ex){
cout << ex.what() << endl;
}
system("pause");
return EXIT_SUCCESS;
}
7.3.2 編寫自己的異常類
編寫自己的異常類署惯?
① 標準庫中的異常是有限的;
② 在自己的異常類中镣隶,可以添加自己的信息极谊。(標準庫中的異常類值允許設置一個用來描述異常的字符串)。如何編寫自己的異常類安岂?
① 建議自己的異常類要繼承標準異常類轻猖。因為C++中可以拋出任何類型的異常,所以我們的異常類可以不繼承自標準異常域那,但是這樣可能會導致程序混亂咙边,尤其是當我們多人協(xié)同開發(fā)時。
② 當繼承標準異常類時次员,應該重載父類的what
函數和虛析構函數败许。
③ 因為棧展開的過程中,要復制異常類型淑蔚,那么要根據你在類中添加的成員考慮是否提供自己的復制構造函數市殷。
//自定義異常類
class MyOutOfRange:public exception
{
public:
MyOutOfRange(const string errorInfo)
{
this->m_Error = errorInfo;
}
MyOutOfRange(const char * errorInfo)
{
this->m_Error = string( errorInfo);
}
virtual ~MyOutOfRange()
{
}
virtual const char * what() const
{
return this->m_Error.c_str() ;
}
string m_Error;
};
class Person
{
public:
Person(int age)
{
if (age <= 0 || age > 150)
{
//拋出異常 越界
//cout << "越界" << endl;
//throw out_of_range("年齡必須在0~150之間");
//throw length_error("長度異常");
throw MyOutOfRange(("我的異常 年齡必須在0~150之間"));
}
else
{
this->m_Age = age;
}
}
int m_Age;
};
void test01()
{
try
{
Person p(151);
}
catch ( out_of_range & e )
{
cout << e.what() << endl;
}
catch (length_error & e)
{
cout << e.what() << endl;
}
catch (MyOutOfRange e)
{
cout << e.what() << endl;
}
}