概述
今天聽了項目組裏的C++高手講C++的異常,受益匪淺。果然,與高手一起才能學習到更多的東西。下面我就把這位高手介紹的C++異常處理分享給園子裏的博友們。
什麼是異常呢?
在編程語言裏,按照出現錯誤的時機來區分,有編譯期錯誤和運行期錯誤之分。
編譯期錯誤大家肯定很熟悉了,當我們build一個程序時,console裏出現的那些error提示就是編譯期錯誤。這些錯誤是在編譯期就能被編譯器檢查出來。
運行期錯誤大家可能不太經常見,因爲自己寫的程序往往都是在“溫室”裏運行的,很少看到程序崩潰的情況。運行期錯誤的種類很多,舉個例子,當我們要在堆上申請內存的時候,內存空間不足,或者在創建文件的時候,磁盤空間不足,這些都是運行期錯誤。我們用一個名字——異常來稱呼這樣的運行期錯誤。
C++如何捕獲異常?
C++有自己異常處理體系,它捕獲異常的語法與java和C#很相似,可以看下面的代碼:
1 int main(){ 2 3 try{ 4 cout<<"This an easy exception example."<<endl; 5 throw 1; 6 }catch(int i){ 7 cout<<"Catched the exception: i = "<<i<<endl; 8 } 9 10 return 0; 11 }
語法很簡單,用try block來包含要捕獲異常區域的代碼,這段代碼裏會有throw語句拋出異常,再用catch來捕獲異常,上面代碼的輸出結果如下:
This an easy exception example. Catched the exception: i = 1 請按任意鍵繼續. . .
慎用catch(...)
C++的異常捕獲提供了一個萬能捕獲器,就是catch(...),它可以捕獲任意的異常,可以看出來,因爲沒有參數名,這樣我們就沒辦法獲取異常傳遞過來的內容了。不過還有一個重要的問題,就是catch(...)的位置問題。下面看一段代碼:
1 try{ 2 cout<<"This an easy exception example."<<endl; 3 throw 1; 4 }catch(...){ 5 cout<<"Catched the exception."<<endl; 6 }catch(int i){ 7 cout<<"Catched the exception: i = "<<i<<endl; 8 }
上面的代碼把catch(...)放到了catch(int i)之前,這樣有什麼問題呢?catch(int i)包含的異常處理塊永遠不會被執行。強悍一點的編譯器會爲我們指出錯誤,但是有些編譯器就沒那麼強大了。所以,記住一條準則:catch(...)永遠放到所有捕獲catch處理的最後一個。
慎用繼承體系裏的類作爲catch的參數
這個問題跟上面的問題類似,也是catch的優先級問題,看下面的代碼:
1 try{ 2 DerivedClass dc; 3 cout<<"This an easy exception example."<<endl; 4 throw dc; 5 }catch(SuperClass s){ 6 cout<<"Catched the exception:SuperClass."<<endl; 7 }catch(DerivedClass d){ 8 cout<<"Catched the exception:DerivedClass."<<endl; 9 }
上面的代碼中,我們拋出了DerivedClass類的對象,本以爲會進入catch(DerivedClass d)的處理塊的,但是事實上它之調用了catch(SuperClass s)的處理塊。這個代碼編譯器不會去檢查,只能靠我們自己把握了,記住一個準則:要把最高級別的父類放到最後一個Catch裏處理。
對象析構函數被調用的三種場合?
對象析構函數什麼時候會被調用呢?這裏先說一下,有三種情況析構函數被調用。哪三種呢?先看我們熟悉的兩種:
1 void func(){ 2 A a; 3 }
第一種:上面的函數在調用時,函數完成調用後,會自動調用A的對象a的析構函數。
1 A *a = new A(); 2 delete a;
第二種:顯示的調用delete語句也會調用對象的析構函數。
那第三種是什麼呢?其實你已經看到了,就是異常處理區域的throw語句,看下面的代碼:
1 try{ 2 DerivedClass dc; 3 cout<<"This an easy exception example."<<endl; 4 throw dc; 5 }
上面的代碼,throw語句實際上爲我們調用了一次析構函數,儘管這個函數後面可能還有語句。實際上在拋出一個對象的時候,異常體系已經複製了一個tem_dc的對象。然後再調用DerivedClass的析構函數。所以,下面的代碼讓我們感到很惱怒:
1 try{ 2 DerivedClass *pDc = new DerivedClass(); 3 throw pDc; 4 }
我們希望把pDc指向的對象throw出來,實際上我們只是拋出了pDc這個指針,而這個對象早已經被析構掉了。所以這裏記住一個準則:儘量不要拋出指針和引用。
不要在異常處理體系中寄希望與類型轉換
不要期望異常處理體系爲我們完成類型轉換,看下面的代碼:
1 try{ 2 throw 'a'; 3 }catch(int ch){ 4 cout<<"this is a ch"<<endl; 5 }
平時寫代碼的時候,'a'是可以轉換成asic碼值的,但是這個時候就不行了,程序運行期是錯誤的。所以,記住一個準則:不要寄希望於C++異常處理體系會幫你做類型轉換。
有C++異常處理體系捕獲不到的東西嗎?
有的,但也沒有,什麼意思呢。本來有的,後來被解決了。不知道你還記不記得住C++的成員初始化列表,不記得的話,咱們看下面的代碼:
1 class Test{ 2 private: 3 int age; 4 public: 5 Test():age(initialze(1)){ 6 7 } 8 int initialze(int i){ 9 if(i == 1){ 10 throw 1; 11 }else{ 12 return 1; 13 } 14 } 15 };
上面的代碼中,構造函數在成員初始化列表中調用了可能拋出異常的initialize函數,這樣的異常怎麼捕獲呢?用前輩的一句話:C++於是提供了一種很醜的語法來解決這個問題。怎麼解決的呢?看下面的代碼:
1 Test() 2 try:age(initialze(1)){ 3 { 4 //函數體 5 } 6 }catch(int i){ 7 cout<<"exception"<<endl; 8 }
是不是很醜陋的語法,我也覺得怎麼好看,不過確實捕獲了成員初始化列表的異常。
set_unexpected函數的用處
這個函數的作用,簡而言之,就是設置默認異常處理函數。什麼意思呢?看下面的代碼就瞭解了。
1 void myFunc(){ 2 cout<<"set_unexpected Exception."<<endl; 3 throw 0; 4 } 5 void fun(int x) throw(char) 6 { 7 throw 'a'; 8 } 9 int main(){ 10 set_unexpected(myFunc); 11 try{ 12 fun(1); 13 }catch(int i){ 14 cout<<"int exception"<<endl; 15 } 16 17 system("pause"); 18 return 0; 19 }
從main開始看,我們註冊了一個默認異常處理函數,這個函數會對異常做一個修正。fun函數裏拋出char的異常,我們的語句是捕獲不了的,所以經過默認處理函數修正之後,就可以用catch(int i)捕獲了。
不過,上面的代碼在VS上運行是不行的,linux下運行就okay了。
Effective C++:不要讓異常逃離析構函數
看懂了上面的,你就已經很厲害啦,當然這只是個玩笑。EffiectiveC++裏有一條:不要讓異常逃離析構函數。什麼意思呢?就是當我們遇到下面這樣的代碼後:
1 try{ 2 DerivedClass dc; 3 cout<<"This an easy exception example."<<endl; 4 throw dc; 5 }catch(SuperClass s){ 6 cout<<"Catched the exception:SuperClass."<<endl; 7 }catch(DerivedClass d){ 8 cout<<"Catched the exception:DerivedClass."<<endl; 9 }
上面的代碼throw dc之後,會調用DerivedClass的析構函數,這樣的話,如果析構函數再拋出異常,我們的捕獲函數就悲劇的不知道該如何處理了。也就是說,當同時出現兩個throw拋出的異常之後,程序就會直接宕掉。所以,不要讓異常逃離析構函數,否則,你就悲劇了。
轉自:http://www.cnblogs.com/alephsoul-alephsoul/archive/2012/11/30/2796905.html