【原創】Performanced C++ 經驗規則 第三條:你不知道的構造函數(下)


第三條:你不知道的構造函數(下)

前面兩篇,我們已經討論了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++的壁壘,我們又添加了一把強有力的斧頭。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM