程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> C++入門知識 >> C++華麗的exception handling(異常處理)背後隱藏的陰暗面及其處理方法

C++華麗的exception handling(異常處理)背後隱藏的陰暗面及其處理方法

編輯:C++入門知識

前言
最近在看auto_ptr源碼的時候,發現裡面的異常說明很多。事實上對於exception handling這塊,以前也有很多困惑的地方,只是由於平時代碼中很少用到,於是就從來沒仔細鑽研過。本來這篇是用來寫smart pointer的,既然遇到了exception handling這塊,那麼先把這塊硬骨頭啃下來再說吧。
翻閱了很多大師的經典著作,發現exception handling在《c++ primer》中只是概念性的提了下,對於技巧型的內容幾乎沒有涉及到;《effective c++》中只有一個條款中提及,《inside c++ object model》也提到很少,幸運的是《more effective c++》中卻有一個專題來研討這塊,而且講到很多技巧性的內容,令人脍炙人口。。 雖然新公司裡很少用到exception handling,所有的狀態信息都是以日志文件形式來記錄,而誰也不能保證以後的工作中不會用到,對於一般性的異常處理,自認為還是可以應付的來的,至少不會導致因為沒有處理的exception而teminate了當前程序,而如果要寫出高質量高穩定性的C++代碼,不掌握exception handling的技巧性使用應該是很難的(至少我是這麼認為的),當然了,C陣營中錯誤代碼或返回狀態信息是另一類exception技能了。而正如Scott Meyers在《effective c++》的條款一所說:視C++為一個語言聯邦;因而不該因為C++是由C發展而來而全盤忽視了C++的特性。。。
 
VS編譯器對異常規范的忽視
異常規范在《C++ Primer》中倒是提及的多一些。這一塊也是exception handling中最令人頭大的一塊,因為盡管編譯器能檢測出少量異常規范,對於大多數的異常規范,只有在運行期才能知曉,如果違反了異常規范,諸多大師的一致解釋是:程序會自動調用標准庫的unexpacted函數,此函數又調用teminate函數,從而直接終止程序的運行。來看看如下代碼:
1.    #include <iostream>
2.   
3.    using namespace std;
4.   
5.   
6.    typedef void (*CallBackPtr)(int nEventLocationX,int nEventLocationY,void *pDataToPassValue)throw();
7.   
8.    class CallBack
9.    {
10.   public:
11.       CallBack(CallBackPtr fPtr,void *pDataToPassValue):func(fPtr),pData(pDataToPassValue)
12.       {}
13.       void MakeCallBack(int nEventLocationX,int nEventLocationY) const throw()
14.       {
15.           func(nEventLocationX,nEventLocationY,pData);
16.       };
17.  
18.   private:
19.       CallBackPtr func;
20.       void *pData;
21.   };
22.  
23.   void MyFunc(int nEventLocationX,int nEventLocationY,void *pDataToPassValue) throw(runtime_error)
24.   {
25.       cout<<nEventLocationX<<" "<<nEventLocationY<<endl;
26.       throw runtime_error("runtime error example!");
27.   }
28.  
29.   int main(int *argc , char **argv)
30.   {
31.       CallBackPtr Func = MyFunc;
32.       
33.       CallBack MyCallBack(Func,NULL);
34.       MyCallBack.MakeCallBack(10,10);
35.       return 0;
36.   }
 
這是一個回調函數管理類的例子,是《more effective c++》的原例,如果按照Lippman在《C++ Primer》中所說的話,此程序應該不能通過編譯,因為它存在一個編譯期就能檢測出來的違反異常規范的地方:對於函數MyFunc和函數定義CallBackPtr異常聲明,MyFunc的規范更為嚴格,它應該不能轉化為CallBackPtr的對象才是,因為CallBackPtr的異常規范為throw(),意味著此函數不會拋出任何異常。。而我在VS2008下卻能正常編譯通過,只有一個warning:C++ exception specification ignored except to indicate a function is not __declspec(nothrow),MSDN中對其解釋是:“使用異常規范聲明函數,Visual C++ 接受但並不實現此規范。包含在編譯期間被忽略的異常規范的代碼可能需要重新編譯和鏈接,以便在支持異常規范的未來版本中重用。”意即VC編譯器不支持異常規格說明。。 令我牽腸掛肚的是:倘若一直如此的話,那麼C++的異常規范特性得全部由程序員來掌控,感歎VC編譯器對於這點多少有些不人道。。 但我也不想因此而以點蓋面的全盤否定VC編譯器在其它方面的高性能。
使用smart pointer來防止destructor中的資源洩露
對於由於異常處理不當而引發的資源洩露,無疑亦是程序員喜歡討論的話題之一,因為拋出異常意味著一個拋異常的代碼塊可能只執行了一部分(前提是當前函數沒有處理異常),這樣的話,那麼異常又會傳送到當前代碼塊的外圍去處理,而引發資源洩露的代碼塊往往卻是這塊沒被執行的代碼,看看如下例子:
1.    #include <iostream>
2.    using namespace std;
3.   
4.    class BaseClass
5.    {
6.    public:
7.        BaseClass(){};
8.        ~BaseClass(){};
9.    };
10.  
11.   void ExceptionFunc() throw(runtime_error)
12.   {
13.       throw runtime_error("example exception handling!");
14.   }
15.  
16.   void Function() throw(runtime_error)
17.   {
18.       BaseClass *pBase = new BaseClass;
19.  
20.       ExceptionFunc();
21.       
22.       delete pBase;
23.   }
24.  
25.   int main(int *argc , char **argv)
26.   {
27.       try
28.       {
29.           Function();
30.       }
31.       catch(runtime_error &err)
32.       {
33.           cout<<err.what()<<endl;
34.       }
35.       return 0;
36.   }
 
不得不承認在main函數返回之前,所有的異常確實得到了處理,讓人難以忽視的是:Function裡面拋出異常後,delete pBase沒有執行,這就意味著發生了內存洩露。。指針無處不在,如果我們不想用指針而想提高代碼的效率和質量,那幾乎是不可能的。事實上,我們可以在Function函數裡捕捉異常,然後在異常處理塊中執行delete pBase,可以避免由此引發的內存洩露,然而這樣做的缺陷是要寫兩個delete,Scott Meyers對於這種引發內存的更好的處理方式是:使用smart pointer。 如果將Function改為如下:
1.    void Function() throw(runtime_error)
2.    {
3.        BaseClass *pBase = new BaseClass;
4.        auto_ptr<BaseClass> PtrBase(pBase);
5.        ExceptionFunc();
6.    }
 
用類來管理資源是防止資源洩露的有力法器之一,這種情況下異常拋出後,auto_ptr對象肯定會執行析構函數,此時會自動釋放其指針成員指向的對象資源,即便它的對象為NULL,由於C++保證了delete空指針無異常的特性,所以資源是肯定會正確的釋放。然而在我看來,smart pointer的使用也只是一種折中而已,因為使用auto_ptr而帶來的負面性後果其實也可以大作討論了,有待我之後的smart pointer文章再作詳細討論。 
異常逃離destructor的災難性後果
這一點也是唯一一條Sotte Meyyers在《effective c++》(條款8)和《more effective c++》(第五章節)中重復討論了兩次的條款,當destructor中無法處理異常的話,程序會直接調用teminate從而終止。。如果試圖在destructor外部捕獲異常,那將是徒勞的,正如一般重載delete運算符的聲明式一樣,往往在後面又加個異常規范throw(),這意味著delete外部根本無法捕捉到其內部的異常。看看下面這個簡單例子:
1.    #include <memory>
2.    #include <iostream>
3.    using namespace std;
4.    class BaseClass
5.    {
6.    public:
7.       BaseClass(){};
8.        ~BaseClass()
9.        {
10.           throw runtime_error("example runtime error.");
11.       };
12.   };
13.  
14.   int main(int *argc , char **argv)
15.   {
16.       BaseClass *pBase = new BaseClass;
17.       delete pBase;
18.       return 0;
19.   }
 
在VS2008下調用teminate時候還會調用abort,這個程序會非正常結束,如果在main函數中試圖這樣做:
1.    int main(int *argc , char **argv)
2.    {
3.        BaseClass *pBase = new BaseClass;
4.        try
5.        {
6.            delete pBase;
7.        }
8.        catch(runtime_error &err)
9.        {
10.           cout<<err.what()<<endl;
11.       }
12.       return 0;
13.   }
 
結果會跟上面一樣(非正常結束),因為delete是不會將任何異常傳遞到其外面的;一種比較折中的解決方法是,當destructor中存在異常拋出時,在destructor最後添加一個能捕獲所有異常的catch處理塊,catch處理塊又什麼工作都不做,如下:
 
1.    ~BaseClass()
2.    {
3.        try
4.        {
5.            throw runtime_error("error in destructor");
6.        }
7.        catch(...)
8.        {
9.        }
10.   };
看起來是一種很壞很無奈的辦法,但正如Scott Meyers在《effective c++》中所說:
“一般而言,將異常吞掉是個壞主意,因為它壓制了"某些動作失敗"的重要信息!然而有時候吞下異常也比負擔"草率結束程序"或"不明確行為帶來的風險好”。
 
 
後記
對於很多exception handling的概念性細節(比如何時使用引用類型的異常捕捉、異常捕獲層次的類型轉換等等)我沒做任何闡述,可以去看看《C++ PRIMER》的第十七章,有著很想盡的講解。。。 對於MS編譯器對異常規范的不支持,我很難理解,因為G++編譯器確實是支持的。之前在討論C++的object布局時(點擊這裡)也曾感歎MS的編譯器在優化方面沒G++走得快,對於這些,或許是我運氣不好,老是碰到MS不如G++的地方,也或許是我現在幾乎不用G++編譯器的而體會不到其不如MS編譯器的地方的緣故吧。。。exception handling的確能為提高代碼質量的改善作出或多說少的貢獻,但華麗麗的外表下,因為用不好它而導致的程序的很多不明確(如teminate當前程序)和不正常(如資源洩露)行為也是令人比較頭大的地方。貌似只有多熟用有技巧性的用是唯一能解決所有問題的方法了。。
 
 
本文出自 “酋長” 博客
 

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved