第三條:你不知道的構造函數(下)
前面兩篇,我們已經討論了C++構造函數中諸多細枝末節,但百密一疏,還有一些地方我們沒有考慮到。這一篇將對這些問題進行完結。
7、構造函數中的異常
當你在構造函數中寫代碼的時候,你有沒有想過,如果構造函數中出現異常(別告訴我,你不拋異常。“必要”時系統會替你拋的),那會出現怎樣的情況?
對象還能構建完成嗎?構造函數中已經執行的代碼產生的負面效應(如動態分配內存)如何解決?對象退出其作用域時,其析構函數能被調用嗎?
上述這些問題,正是構造函數中產生異常要面臨的問題。讓我們先看結論,再分析過程:盡可能不要在構造函數中產生(拋出)異常,否則,一定會產生問題。
我們先看一段代碼:
1 #include <iostream> 2 #include <exception> 3 #include <stdexcept> 4 using namespace std; 5 6 7 class ConWithException 8 { 9 public: 10 ConWithException() : _pBuf(NULL) 11 { 12 _pBuf = new int[100]; 13 throw std::runtime_error("Exception in Constructor!"); 14 } 15 16 ~ConWithException() 17 { 18 cout << "Destructor!" << endl; 19 if( _pBuf != NULL ) 20 { 21 cout << "Delete buffer..." << endl;; 22 delete[] _pBuf; 23 _pBuf = NULL; 24 } 25 } 26 27 private: 28 int* _pBuf; 29 }; 30 31 int main(int argc, char** argv) 32 { 33 ConWithException* cwe = NULL; 34 try 35 { 36 cwe = new ConWithException; 37 } 38 catch( std::runtime_error& e ) 39 { 40 cout<< e.what() << endl; 41 } 42 43 delete cwe; 44 45 return 0; 46 } 47
這段代碼運行結果是什么呢?
輸出
1 Exception in Constructor!
輸出“Exception in Constructor!"說明,我們拋出的異常已經成功被捕獲,但有沒有發現什么問題呢?有一個很致命的問題,那就是,對象的析構函數沒有被調用!也就是說,delete cwe這一句代碼沒有起任何作用,相當於對delete NULL指針。再往上推,我們知道cwe值還是初始化的NULL,說明對象沒有成功的構建出來,因為在構造函數中拋出了異常,終止了構造函數的正確執行,沒有返回對象。即使我們把cwe = new ConWithException換成在棧中分配(ConWithException cwe;),仍是相同的結果,但cwe退出其作用域時,其析構函數也不會被調用,因為cwe根本不是一個正確的對象!繼續看,在這個構造函數中,為成員指針_pBuf動態申請了內存,並計划在析構函數中釋放這一塊內存。然而,由於構造函數拋出異常,沒有返回對象,析構函數也沒有被調用,_pBuf指向的內存就發生了泄露!每調用一次這個構造函數,就泄露一塊內存,產生嚴重的問題。現在,你知道了,為什么不能在構造函數中拋出異常,即使沒有_pBuf這樣需要動態申請內存的指針成員存在。
然而很多時候,異常並不是由你主動拋出的,也就是說,將上述構造函數改造成這樣:
ConWithException() : _pBuf(NULL) { _pBuf = new int[100]; }
這是我們十分熟悉的格式吧?沒錯,但是,這樣的寫法仍然可能產生異常,因為這取決於編譯器的實現。當動態內存分配失敗時,編譯器可能返回一個NULL指針(這也是慣用方式),OK,那沒有問題。但是,有些編譯器也有可能引發bad_alloc異常,如果對異常進行捕獲(通常也不會這樣做),結果將同上述例子所示。而如果未對異常進行捕獲,結果更加糟糕,這將產生Uncaught exception,通常將導致程序終止。並且,此類問題是運行階段可能出現的問題,這將更難發現和處理。
說了半天,就是認為上述寫法,還不夠好,不OK,接下來講述解決方案。
解決方案一:使用智能指針shared_ptr(c++0x后STL提供,c++0x以前可采用boost),注意,在此處不能使用auto_ptr(因為要申請100個int,而即使申請的是單個對象,也不建議使用auto_ptr,關於智能指針,本系列后面的規則會有講述);
解決方案二:就是前面多次提到的,采用"工廠模式"替換公有構造函數,從而盡可能使構造函數“輕量級“。
class ConWithException //為和前面比對,類名沒改,糟糕的類名 { public: ConWithException* factory(some parameter...) { ConWithException* cwe = new ConWithException; if(cwe) { cwe->_pBuf = new int[100]; //other initialization... } return cwe; } ~ConWithException() { if(cwe->_pBuf) { delete[] cwe->_pBuf; _pBuf = NULL; } //other destory process... } private: ConWithException() : _pBuf(NULL) {} //如果有非靜態const成員還需要在初始化列表中進行初始化,否則什么也不做 int* _pBuf; };
使用“工廠模式”的好處是顯而易見的,上述構造函數中異常的問題可以得到完美解決?why?因為構造函數十分輕量級,可輕松的完成對象的構建,“重量級”的工作都交由“工廠”(factory)方法完成,這是一個公有的普通成員函數,如果在這個函數中產生任何異常,因為對象已經正確構建,可以完美的進行異常處理,也能保證對象的析構函數被正確地調用,杜絕memory leak。構造函數被聲明為私有,以保證從工廠“安全”地產生對象,使用“工廠模式”,還可以禁止從棧上分配對象(其實Java、Objective-C都是這么做的),在必要的時候,這會很有幫助。
8、構造函數不能被繼承:雖然子類對象中包含了基類對象,但並不能代表構造函數被繼承,即,除了在子類構造函數的初始化列表里,你可以顯式地調用基類的構造函數,在子類的其它地方調用父類的構造函數都是非法的。
9、當類中有需要動態分配內存的成員指針時,需要使用“深拷貝“重寫拷貝構造函數和賦值操作符,杜絕編譯器“用心良苦”的產生自動生成版本,以防資源申請、釋放不正確。
10、除非必要,否則最好在構造函數前添加explicit關鍵字,杜絕隱式使構造函數用作自動類型轉換。
終於寫完了,這三篇有關構造函數的“經驗”之談,其實,這些問題,也是老生常談了。經過這三篇的學習,為敲開C++的壁壘,我們又添加了一把強有力的斧頭。