前面兩篇,我們已經討論了C++建構函式中諸多細枝末節,但百密一疏,還有一些地方我們沒有考慮到。這一篇將對這些問題進行完結。
7、建構函式中的異常
當你在建構函式中寫程式碼的時候,你有沒有想過,如果建構函式中出現異常(別告訴我,你不拋異常。“必要”時系統會替你拋的),那會出現怎樣的情況?
物件還能構建完成嗎?建構函式中已經執行的程式碼產生的負面效應(如動態分配記憶體)如何解決?物件退出其作用域時,其解構函式能被呼叫嗎?
上述這些問題,正是建構函式中產生異常要面臨的問題。讓我們先看結論,再分析過程:儘可能不要在建構函式中產生(丟擲)異常,否則,一定會產生問題。
我們先看一段程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
#include <iostream> #include <exception> #include <stdexcept> using namespace std; class ConWithException { public: ConWithException() : _pBuf(NULL) { _pBuf = new int[100]; throw std::runtime_error("Exception in Constructor!"); } ~ConWithException() { cout << "Destructor!" << endl; if( _pBuf != NULL ) { cout << "Delete buffer..." << endl;; delete[] _pBuf; _pBuf = NULL; } } private: int* _pBuf; }; int main(int argc, char** argv) { ConWithException* cwe = NULL; try { cwe = new ConWithException; } catch( std::runtime_error& e ) { cout<< e.what() << endl; } delete cwe; return 0; } |
這段程式碼執行結果是什麼呢?
輸出
1 |
Exception in Constructor! |
輸出“Exception in Constructor!”說明,我們丟擲的異常已經成功被捕獲,但有沒有發現什麼問題呢?有一個很致命的問題,那就是,物件的解構函式沒有被呼叫!也就是說,delete cwe這一句程式碼沒有起任何作用,相當於對delete NULL指標。再往上推,我們知道cwe值還是初始化的NULL,說明物件沒有成功的構建出來,因為在建構函式中丟擲了異常,終止了建構函式的正確執行,沒有返回物件。即使我們把cwe = new ConWithException換成在棧中分配(ConWithException cwe;),仍是相同的結果,但cwe退出其作用域時,其解構函式也不會被呼叫,因為cwe根本不是一個正確的物件!繼續看,在這個建構函式中,為成員指標_pBuf動態申請了記憶體,並計劃在解構函式中釋放這一塊記憶體。然而,由於建構函式丟擲異常,沒有返回物件,解構函式也沒有被呼叫,_pBuf指向的記憶體就發生了洩露!每呼叫一次這個建構函式,就洩露一塊記憶體,產生嚴重的問題。現在,你知道了,為什麼不能在建構函式中丟擲異常,即使沒有_pBuf這樣需要動態申請記憶體的指標成員存在。
然而很多時候,異常並不是由你主動丟擲的,也就是說,將上述建構函式改造成這樣:
1 2 3 4 |
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,關於智慧指標,本系列後面的規則會有講述);
解決方案二:就是前面多次提到的,採用“工廠模式”替換公有建構函式,從而儘可能使建構函式“輕量級“。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
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++的壁壘,我們又新增了一把強有力的斧頭。