Performanced C++ 經驗規則(3):你不知道的構造函數(下)

jopen 8年前發布 | 7K 次閱讀 C/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++的壁壘,我們又添加了一把強有力的斧頭。

 
 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!