熱線電(dian)話:0755-23712116
郵(you)箱:contact@legoupos.cn
地址:深圳市寶安(an)區沙井街道(dao)后亭茅洲山工業園(yuan)工業大廈(sha)全(quan)至科技創新園(yuan)科創大廈(sha)2層2A
程序運行時常會碰(peng)到一(yi)些異(yi)常情況,例(li)如:
? 做除法的時(shi)候除數為 0;
? 用戶(hu)輸(shu)入年齡時(shi)輸(shu)入了一個負數;
? 用 new 運算符(fu)動態分配空(kong)間時,空(kong)間不夠導致無法分配;
? 訪(fang)問數組元(yuan)素時,下標(biao)越界;打開文件讀(du)取時,文件不存在。
這些異常情況,如果不能發現并加以處理,很可能會導致程序崩潰。
所謂“處理”,可以是給出錯誤提示信息,然后讓程序沿一條不會出錯的路徑繼續執行;也可能是不得不結束程序,但在結束前做一些必要的工作,如將內存中的數據寫入文件、關閉打開的文件、釋放動態分配的內存空間等。
一發現異常情況就立即處理未必妥當,因為在一個函數執行過程中發生的異常,在有的情況下由該函數的調用者決定如何處理更加合適。尤其像庫函數這類提供給程序員調用,用以完成與具體應用無關的通用功能的函數,執行過程中貿然對異常進行處理,未必符合調用它的程序的需要。
此外,將異常分散在各處進行處理不利于代碼的維護,尤其是對于在不同地方發生的同一種異常,都要編寫相同的處理代碼也是一種不必要的重復和冗余。如果能在發生各種異常時讓程序都執行到同一個地方,這個地方能夠對異常進行集中處理,則程序就會更容易編寫、維護。
鑒于上述原因,C++ 引入了異常處理機制。其基本思想是:函數 A 在執行過程中發現異常時可以不加處理,而只是“拋出一個異常”給 A 的調用者,假定為函數 B。
拋出異常而不加處理會導致函數 A 立即中止,在這種情況下,函數 B 可以選擇捕獲 A 拋出的異常進行處理,也可以選擇置之不理。如果置之不理,這個異常就會被拋給 B 的調用者,以此類推。
如(ru)果一層(ceng)層(ceng)的(de)函(han)(han)(han)數(shu)都不處理(li)異常(chang),異常(chang)最終會(hui)被(bei)拋給最外層(ceng)的(de)main 函(han)(han)(han)數(shu)。main 函(han)(han)(han)數(shu)應該處理(li)異常(chang)。如(ru)果main函(han)(han)(han)數(shu)也不處理(li)異常(chang),那么程序(xu)就會(hui)立(li)即異常(chang)地中止。
C++異常處理基本語法
C++通過 throw 語(yu)句和 try...catch 語(yu)句實(shi)現對異常的(de)處理。throw 語(yu)句的(de)語(yu)法如下:
throw 表達式;
該語句拋出一個異常。異常是一個表達式,其值的類型可以是基本類型,也可以是類。
try...catch 語句的語法如下:
try {
語句組
}
catch(異常類型) {
異常處理代碼
}
...
catch(異常類型) {
異常處理代碼
}
catch可以有多個,但至少要有一個。
不妨把 try 和其后{}中的內容稱作“try塊”,把 catch 和其后{}中的內容稱作“catch塊”。
try...catch 語句的執行過(guo)程(cheng)是:
? 執(zhi)行 try 塊中(zhong)的(de)(de)語(yu)句(ju),如(ru)果執(zhi)行的(de)(de)過程中(zhong)沒(mei)有異常拋出,那么執(zhi)行完后就執(zhi)行最(zui)后一(yi)個 catch 塊后面的(de)(de)語(yu)句(ju),所有 catch 塊中(zhong)的(de)(de)語(yu)句(ju)都不會被執(zhi)行;
? 如果 try 塊執行的(de)(de)過程(cheng)中拋(pao)出(chu)了(le)異常(chang),那么拋(pao)出(chu)異常(chang)后(hou)(hou)立即跳轉到(dao)第一個“異常(chang)類(lei)型”和拋(pao)出(chu)的(de)(de)異常(chang)類(lei)型匹配的(de)(de) catch 塊中執行(稱作(zuo)異常(chang)被(bei)該(gai) catch 塊“捕獲”),執行完(wan)后(hou)(hou)再跳轉到(dao)最后(hou)(hou)一個 catch 塊后(hou)(hou)面繼(ji)續(xu)執行。
例如下面的程序:
#includeug namespace std; int main() { double m ,n; cin >> m >> n; try { cout << "before dividing." << endl; if( n == 0) throw -1; //拋出int類型異常 else cout << m / n << endl; cout << "after dividing." << endl; } catch(double d) { cout << "catch(double) " << d << endl; } catch(int e) { cout << "catch(int) " << e << endl; } cout << "finished" << endl; return 0; }
程序的運行結果如下:
9 6↙
before dividing.
1.5
after dividing.
finished
說明當 n 不為 0 時,try 塊中不會拋出異常。因此程序在 try 塊正常執行完后,越過所有的 catch 塊繼續執行,catch 塊一個也不會執行。
程序的運行結果也可能如下:
9 0↙
before dividing.
catch\(int) -1
finished
當 n 為 0 時,try 塊中會拋出一個整型異常。拋出異常后,try 塊立即停止執行。該整型異常會被類型匹配的第一個 catch 塊捕獲,即進入catch(inte)塊執行,該 catch 塊執行完畢后,程序繼續往后執行,直到正常結束。
如(ru)(ru)果拋出的(de)異常(chang)沒(mei)有被 catch 塊(kuai)捕獲(huo),例如(ru)(ru),將catch(int e),改為catch(chare),當輸入的(de) n 為 0 時(shi),拋出的(de)整型異常(chang)就(jiu)沒(mei)有catch 塊(kuai)能(neng)捕獲(huo),這個異常(chang)也就(jiu)得不到(dao)處理,那么(me)程序就(jiu)會立(li)即中止,try...catch 后面的(de)內容都(dou)不會被執行。
能(neng)夠捕獲任(ren)何異常的 catch 語句
如果希望(wang)不論拋(pao)出哪(na)種(zhong)類型的異常都能捕獲(huo),可(ke)以(yi)編寫如下 catch 塊:
catch(...) {
...
}
這樣的catch 塊能(neng)夠(gou)捕(bu)獲(huo)任何還沒有被(bei)捕(bu)獲(huo)的異常。例如下面的程序:
#includeusing namespace std; int main() { double m, n; cin >> m >> n; try { cout << "before dividing." << endl; if (n == 0) throw - 1; //拋出整型異常 else if (m == 0) throw - 1.0; //拋出 double 型異常 else cout << m / n << endl; cout << "after dividing." << endl; } catch (double d) { cout << "catch (double)" << d << endl; } catch (...) { cout << "catch (...)" << endl; } cout << "finished" << endl; return 0; }
程序的運行結果如下:
9 0↙
before dividing.
catch (...)
finished
當 n 為 0 時,拋出的整型異常被catchy(...)捕獲。
程序的運行結果也可能如下:
0 6↙
before dividing.
catch (double) -1
finished
當 m 為 0 時,拋出一個 double類型的異常。雖然catch (double)和catch(...)都能匹配該異常,但是catch(double)是第一個能匹配的 catch 塊,因此會執行它,而不會執行catch(...)塊。
由于catch(...)能(neng)匹配任(ren)何類型的異常,它(ta)后(hou)面(mian)的 catch 塊實際上就不起作用,因此不要(yao)將(jiang)它(ta)寫(xie)在其他 catch 塊前面(mian)。
異常的再拋出
如果一個函數(shu)(shu)(shu)在執行過程中(zhong)拋出的異常(chang)在本函數(shu)(shu)(shu)內就(jiu)被(bei) catch 塊捕獲并處理,那(nei)么該異常(chang)就(jiu)不會(hui)(hui)拋給這個函數(shu)(shu)(shu)的調用(yong)者(zhe)(也(ye)稱為(wei)“上(shang)一層的函數(shu)(shu)(shu)”);如果異常(chang)在本函數(shu)(shu)(shu)中(zhong)沒有被(bei)處理,則它就(jiu)會(hui)(hui)被(bei)拋給上(shang)一層的函數(shu)(shu)(shu)。例如下面的程序:
#include#include using namespace std; class CException { public: string msg; CException(string s) : msg(s) {} }; double Devide(double x, double y) { if (y == 0) throw CException("devided by zero"); cout << "in Devide" << endl; return x / y; } int CountTax(int salary) { try { if (salary < 0) throw - 1; cout << "counting tax" << endl; } catch (int) { cout << "salary < 0" << endl; } cout << "tax counted" << endl; return salary * 0.15; } int main() { double f = 1.2; try { CountTax(-1); f = Devide(3, 0); cout << "end of try block" << endl; } catch (CException e) { cout << e.msg << endl; } cout << "f = " << f << endl; cout << "finished" << endl; return 0; }
程序的輸出結果如下:
salary < 0
tax counted
devided by zero
f=1.2
finished
CountTa 函數拋出異常后自行處理,這個異常就不會繼續被拋給調用者,即 main 函數。因此在 main 函數的 try 塊中,CountTax 之后的語句還能正常執行,即會執行f =Devide(3, 0);。
第 35 行,Devide 函數拋出了異常卻不處理,該異常就會被拋給 Devide 函數的調用者,即 main 函數。拋出此異常后,Devide 函數立即結束,第 14 行不會被執行,函數也不會返回一個值,這從第 35 行 f 的值不會被修改可以看出。
Devide 函數中拋出的異常被 main 函數中類型匹配的 catch 塊捕獲。第 38 行中的e 對象是用復制構造函數初始化的。
如果拋出的異常是派生類的對象,而 catch 塊的異常類型是基類,那么這兩者也能夠匹配,因為派生類對象也是基類對象。
雖然函數也可以通過返回值或者傳引用的參數通知調用者發生了異常,但采用這種方式的話,每次調用函數時都要判斷是否發生了異常,這在函數被多處調用時比較麻煩。有了異常處理機制,可以將多處函數調用都寫在一個 try 塊中,任何一處調用發生異常都會被匹配的 catch 塊捕獲并處理,也就不需要每次調用后都判斷是否發生了異常。
有時(shi),雖然在函數中(zhong)對異常進行了處(chu)(chu)理,但是還是希望(wang)能夠通(tong)知調(diao)用者,以(yi)便讓調(diao)用者知道發生(sheng)了異常,從而可(ke)以(yi)作進一步的處(chu)(chu)理。在 catch 塊中(zhong)拋出異常可(ke)以(yi)滿足這種需要。例如:
#include#include using namespace std; int CountTax(int salary) { try { if( salary < 0 ) throw string("zero salary"); cout << "counting tax" << endl; } catch (string s ) { cout << "CountTax error : " << s << endl; throw; //繼續拋出捕獲的異常 } cout << "tax counted" << endl; return salary * 0.15; } int main() { double f = 1.2; try { CountTax(-1); cout << "end of try block" << endl; } catch(string s) { cout << s << endl; } cout << "finished" << endl; return 0; }
程序的輸出結果如下:
CountTax error:zero salary
zero salary
finished
第 14 行的(de)throw;沒有(you)指明拋出(chu)什么(me)樣的(de)異(yi)常(chang),因此拋出(chu)的(de)就是catch 塊捕(bu)獲(huo)到(dao)的(de)異(yi)常(chang),即 string("zero salary")。這個異(yi)常(chang)會(hui)被 main 函數(shu)中的(de) catch 塊捕(bu)獲(huo)。
函數的異常聲明列(lie)表
為了增強程序的(de)(de)(de)可讀性和可維護性,使程序員在使用一個函(han)(han)數時就能看出這個函(han)(han)數可能會拋(pao)出哪些異常,C++ 允許在函(han)(han)數聲明和定(ding)義時,加上它所(suo)能拋(pao)出的(de)(de)(de)異常的(de)(de)(de)列表,具體(ti)寫(xie)法如(ru)下:
void func() throw (int, double, A, B, C);
或
void func() throw (int, double, A, B, C){...}
上面的寫法表明 func 可能拋出 int 型、double型以及 A、B、C 三種類型的異常。異常聲明列表可以在函數聲明時寫,也可以在函數定義時寫。如果兩處都寫,則兩處應一致。
如(ru)(ru)果異(yi)常聲明(ming)列表(biao)如(ru)(ru)下編寫:
void func() throw ();
則說明func 函數不會拋出任何異常。
一個函數如果不交待能拋出哪些類型的異常,就可以拋出任何類型的異常。
函(han)數如果(guo)拋出了(le)其異(yi)常聲明(ming)列表中沒有的異(yi)常,在編(bian)譯(yi)(yi)時不會引發錯(cuo)(cuo)誤,但在運(yun)行(xing)時, Dev C++ 編(bian)譯(yi)(yi)出來(lai)的程(cheng)序(xu)會出錯(cuo)(cuo);用 Visual Studio 2010 編(bian)譯(yi)(yi)出來(lai)的程(cheng)序(xu)則不會出錯(cuo)(cuo),異(yi)常聲明(ming)列表不起實(shi)際作用。
C++標準異常類
C++標準庫中有一些類代表異常,這些類都是從 exception 類派生而來的。常用的幾個異常類如圖 1 所示。
bad_typeid、bad_cast、bad_alloc、ios_base::failure、out_of_range 都是 exception 類的派生類。C++ 程序在碰到某些異常時,即使程序中沒有寫 throw 語句,也會自動拋出上述異常類的對象。這些異常類還都有名為 what 的成員函數,返回字符串形式的異常描述信息。使用這些異常類需要包含頭文件stdexcept。
下面分別介紹以上幾個(ge)異常(chang)類。本節程序的輸出以 Visual Studio 2010為(wei)準,Dev C++ 編譯(yi)的程序輸出有所不同。
1) bad_typeid
使(shi)用typeid 運(yun)算符時,如(ru)果其操作數是(shi)一個多態(tai)類的(de)指針,而(er)該指針的(de)值為 NULL,則會拋出此(ci)異常。
2) bad_cast
在(zai)用(yong)dynamic_cast 進行(xing)從多態基類對象(或引用(yong))到派生類的(de)引用(yong)的(de)強制類型轉換(huan)時,如(ru)果(guo)轉換(huan)是不安全的(de),則會拋(pao)出(chu)此異常(chang)。程序示例如(ru)下:
#include#include using namespace std; class Base { virtual void func() {} }; class Derived : public Base { public: void Print() {} }; void PrintObj(Base & b) { try { Derived & rd = dynamic_cast (b); //此轉換若不安全,會拋出 bad_cast 異常 rd.Print(); } catch (bad_cast & e) { cerr << e.what() << endl; } } int main() { Base b; PrintObj(b); return 0; }
程序的輸出結果如下:
Bad dynamic_cast!
在 PrintObj 函數(shu)(shu)中,通(tong)過 dynamic_cast 檢測 b 是否引用的是一個 Derived 對象,如(ru)果是,就調(diao)用其 Print 成員函數(shu)(shu);如(ru)果不是,就拋出異常,不會(hui)調(diao)用 Derived::Print。
3) bad_alloc
在用(yong) new運算符進行動態內(nei)存(cun)分配時,如(ru)(ru)果沒有足(zu)夠(gou)的內(nei)存(cun),則會引(yin)發此異常。程(cheng)序示例如(ru)(ru)下:
#include#include using namespace std; int main() { try { char * p = new char[0x7fffffff]; //無法分配這么多空間,會拋出異常 } catch (bad_alloc & e) { cerr << e.what() << endl; } return 0; }
程序的輸出結果如下:
bad allocation
ios_base::failure
在默認狀態(tai)下,輸(shu)(shu)入輸(shu)(shu)出流(liu)對象不會(hui)拋出此異常。如果用流(liu)對象的 exceptions 成員函數設置(zhi)了一(yi)些標志位,則在出現打開文件出錯、讀到輸(shu)(shu)入流(liu)的文件尾(wei)等情況時會(hui)拋出此異常。此處不再贅述。
4) out_of_range
用vector 或(huo) string 的 at 成員函數根據(ju)下標訪問元素時,如(ru)果下標越界(jie),則會拋(pao)出此異常。例如(ru):
#include#include #include #include using namespace std; int main() { vector v(10); try { v.at(100) = 100; //拋出 out_of_range 異常 } catch (out_of_range & e) { cerr << e.what() << endl; } string s = "hello"; try { char c = s.at(100); //拋出 out_of_range 異常 } catch (out_of_range & e) { cerr << e.what() << endl; } return 0; }
程序的輸出結果如下:
invalid vector
invalid string position
如果將v.at(100)換成v[100],將s.at(100)換成s[100],程序就不會引發異常(但可能導致程序崩潰)。因為 at 成員函數會檢測下標越界并拋出異常,而 operator[] 則不會。operator [] 相比 at 的好處就是不用判斷下標是否越界,因此執行速度更快。