Performanced C++ 經驗規則(3):你不知道的建構函式(下)
前面兩篇,我們已經討論了C++建構函式中諸多細枝末節,但百密一疏,還有一些地方我們沒有考慮到。這一篇將對這些問題進行完結。
7、建構函式中的異常
當你在建構函式中寫程式碼的時候,你有沒有想過,如果建構函式中出現異常(別告訴我,你不拋異常。“必要”時系統會替你拋的),那會出現怎樣的情況?
物件還能構建完成嗎?建構函式中已經執行的程式碼產生的負面效應(如動態分配記憶體)如何解決?物件退出其作用域時,其解構函式能被呼叫嗎?
上述這些問題,正是建構函式中產生異常要面臨的問題。讓我們先看結論,再分析過程:儘可能不要在建構函式中產生(丟擲)異常,否則,一定會產生問題。
我們先看一段程式碼:
#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;
}
這段程式碼執行結果是什麼呢?
輸出
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++的壁壘,我們又添加了一把強有力的斧頭。喜歡小編分享的文章的小夥伴可以加下小編主頁的Q群一起交流哦!