1. 程式人生 > >Effective C++第三章總結

Effective C++第三章總結

條款13:以物件管理資源 

例:     

void f()
     { 
         Investment *pInv = createInvestment(); 
         ...                   

        //這裡存在諸多“不定因素”,可能造成delete pInv;得不到執行,這可能就存在潛在的記憶體洩露。
         delete pInv;
     } 

解決方法:把資源放進物件內,我們便可依賴C++的“解構函式自動呼叫機制”確保資源被釋放。   

   許多資源被動態分配於堆內而後被用於單一區塊或函式內。它們應該在控制流離開那個區塊或函式時被釋放。標準程式庫提供的auto_ptr正是針對這種形勢而設計的特製產品。auto_ptr是個“類指標物件”,也就是所謂的“智慧指標”,其解構函式自動對其所指物件呼叫delete。

void f()     

{        

         std::auto_ptr <Investment>  pInv(createInvestment());          ...     

}          //函式退出,auto_ptr呼叫解構函式自動呼叫delete,刪除pInv;無需顯示呼叫delete。    

“以物件管理資源”的兩個關鍵想法:

獲得資源後立刻放進管理物件內(如auto_ptr)。每一筆資源都在獲得的同時立刻被放進管理物件中。“資源取得時機便是初始化時機”(Resource Acquisition Is Initialization;RAII)。

管理物件運用解構函式確保資源被釋放。即一旦物件被銷燬,其解構函式被自動呼叫來釋放資源

  由於auto_ptr被銷燬時會自動刪除它所指之物,所以不能讓多個auto_ptr同時指向同一物件。所以auto_ptr若通過copying函式複製它們,它們會變成NULL,而複製所得的指標將取得資源的唯一擁有權!    

看下面例子:    

std::auto_ptr <Investment>  pInv1(createInvestment());          //pInv1指向createInvestment()返回物;     

std::auto_ptr <Investment>  pInv2(pInv1);                              //現在pInv2指向物件,而pInv1被設為NULL;   

  pInv1 = pInv2;                                                  //現在pInv1指向物件,而pIn2被設為NULL;    

受auto_ptr管理的資源必須絕對沒有一個以上的auto_ptr同時指向它。即“有你沒我,有我沒你”。   

 auto_ptr的替代方案是“引用計數型智慧指標”(reference-counting smart pointer;SCSP)、它可以持續跟蹤共有多少物件指向某筆資源,並在無人指向它時自動刪除該資源。    

TR1的tr1::shared_ptr就是一個"引用計數型智慧指標"。     

void f()     

{          ...         

     std::tr1::shared_ptr <Investment>   pInv1(createInvestment());                      //pInv1指向createInvestment()返回物;         

     std::tr1::shared_ptr <Investment>   pInv2(pInv1);                                           //pInv1,pInv2指向同一個物件;         

     pInv1 = pInv2;                                                                          //同上,無變化         

      ...     

}         //函式退出,pInv1,pInv2被銷燬,它們所指的物件也竟被自動釋放。    

auto_ptr和tr1::shared_ptr都在其解構函式內做delete而不是delete[],也就意味著在動態分配而得的陣列身上使用auto_ptr或tr1::shared_ptr是個潛在危險,資源得不到釋放。也許boost::scoped_array和boost::shared_array能提供幫助。

還有,vector和string幾乎總是可以取代動態分配而得的陣列。    

請記住:

1.為防止資源洩漏,請使用RAII物件,它們在建構函式中獲得資源並在解構函式中釋放資源。

2.兩個常被使用的RAII類分別是auto_ptr和tr1::shared_ptr。後者通常是較佳選擇,因為其拷貝行為比較直觀。若選擇auto_ptr,複製動作會使他(被複制物)指向NULL。   

 

條款14:在資源管理類中小心拷貝行為    

我們在條款13中討論的資源表現在堆上申請的資源,而有些資源並不適合被auto_ptr和tr1::shared_ptr所管理。可能我們需要建立自己的資源管理類。   

  例:     

void lock(Mutex *pm);     //鎖定pm所指的互斥量     

unlock(Mutex *pm);        //將pm解除鎖定     

我們建立的資源管理類可能會是這樣:     

class Lock     {       
public:         

explicit Lock(Mutex *pm) : mutexPtr(pm) 
{ 
       lock(mutexPtr);
}    
    
 ~Lock() 
{ 
       unlock(mutexPtr);  
}     
    
private:         

    Mutex *mutexPtr;     
};  

   但是,如果Lock物件被複制,會發生什麼事???     “當一個RAII物件被複制,會發生什麼事?”

大多數時候你會選擇一下兩種可能:

禁止複製。如果複製動作對RAII類並不合理,你便應該禁止之。禁止類的copying函式參見條款6。

對底層資源使用”引用計數法“。有時候我們又希望保有資源,直到它的最後一個使用者被銷燬。這種情況下複製RAII物件時,應該將資源的”被引用計數“遞增。tr1::shared_ptr便是如此。     

通常只要內含一個tr1::shared_ptr成員變數,RAII類便可實現”引用計數“行為。     

class Lock     
{         
public:             

explicit Lock(Mutex *pm) : mutexPtr(pm, unlock)    
    //由於tr1::shared_ptr預設行為是”當引用計數為0時刪除其所指物“,幸運的是                   
    
//我們可以指定”引用計數“為9時被呼叫的所謂”刪除器“,即第二個引數unlock   
      
{            
    lock(mutexPtr.get());         
}    
     
private:             
    std::tr1::shared_ptr mutexPtr;      

};      

本例中,並沒說明解構函式,因為沒有必要。編譯器為我們生成的解構函式會自動呼叫其non-static成員變數(mutexPtr)的解構函式。而mutexPtr的解構函式會在互斥量”引用計數“為0時自動呼叫tr1::shared_ptr的刪除器(unlock)。     Copying函有可能被編譯器自動創建出來,因此除非編譯器所生成版本做了你想要做的事,否則你得自己編寫它們。    

請記住:

複製RAII物件必須一併複製它所管理的資源,所以資源的copying行為決定RAII物件的copying行為。

普遍而常見的RAII類拷貝行為是:抑制拷貝,施行引用計數法。不過其它行為也可能被實現。   

 

條款15:在資源管理類中提供對原始資源的訪問 

前幾個條款提到的資源管理類很棒。它們是你對抗資源洩漏的堡壘。但這個世界並不完美,許多APIs直接指涉資源,這時候我們需要直接訪問原始資源。     

這時候需要一個函式可將RAII物件(如tr1::shared_ptr)轉換為其所內含之原始資源。有兩種做法可以達成目標:顯示轉換和隱式轉換。    

tr1::shared_ptr和auto_ptr都提供一個get成員函式,用來執行顯示轉換,也就是返回智慧指標內部的原始指標(的復件)。就像所有智慧指標一樣, tr1::shared_ptr和auto_ptr也過載了指標取值操作符(operator->和operator*),它們允許隱式轉換至底部原始指標。(即在對智慧指標物件實施->和*操作時,實際被轉換為被封裝的資源的指標。)    

class Font     

{         

    public:         

    ...         

    FontHandle get() const         //FontHandle 是資源;    顯示轉換函式         

    {            

             return f;         

    }         

    operator FontHandle() const         //隱式轉換    這個值得注意,可能引起“非故意之型別轉換”         

    {             

              return f;        
    
     }        

     ...     

};     

是否該提供一個顯示轉換函式(例如get成員函式)將RAII類轉換為其底部資源,或是應該提供隱式轉換,答案主要取決於RAII類被設計執行的特定工作,以及它被使用的情況。    

顯示轉換可能是比較受歡迎的路子,但是需要不停的get,get;而隱式轉換又可能引起“非故意之型別轉換”。    

請記住:

APIs往往要求訪問原始資源,所以每一個RAII類應該提供一個“取得其所管理之資源”的方法。

對原始資源的訪問可能經由顯示轉換或隱式轉換。一般而言顯示轉換比較安全,但隱式轉換對客戶比較方便。

 

條款16:成對使用new和delete時要採取相同形式    

先看下一下程式碼:     

     std::string *stringArray = new std::string[100];      

     ...     

     delete stringArray;     

使用了new動態申請了資源,也呼叫了delete釋放了資源。但這程式碼存在“不明確行為”。stringArray物件中的99個不太可能被適當刪除,因為它們的解構函式很可能沒被呼叫。     

當我們使用new,有兩件事情發生:第一,記憶體被分配出來;第二,針對此記憶體會有一個(或更多)建構函式被呼叫。

當你使用delete,也有兩件事發生:針對此記憶體會有一個(或多個)解構函式被呼叫,然後記憶體才被釋放。delete的最大問題在於:即將被刪除的記憶體之內究竟有多少物件?這個問題的答案決定了有多少個解構函式必須被呼叫起來。     

解決以上問題事實上很簡單:如果你呼叫new時使用[],你必須在對應呼叫delete時也使用[]。如果你呼叫new時沒有使用[],那麼也不該在對應呼叫delete時使用[]。      最好儘量不要對陣列形式作typedefs動作。因為這樣容易引起delete操作的“疑惑”(需不需要[]呢???)。     

 

請記住:

如果你在new表示式中使用[],必須在相應的delete表示式中也使用[]。如果你在new表示式中不使用[],一定不要在相應的delete表示式中使用[]。     

 

條款17:以獨立語句將newed物件置入智慧指標    

為了避免資源洩漏的危險,最好在單獨語句內以智慧指標儲存newed所得物件。    

即:   

 int priority();     

void processWidget(std::tr1::shared_ptr<Widget> pw, int priority); 

std::tr1::shared_ptr<Widget> pw(new Widget);    //即在傳入函式之前對智慧指標初始化,而不是在傳入引數中                                                                                     //對其初始化,因為那樣可能引起操作序列的問題。    

processWidget(pw, priority());    

 

請記住:

以獨立語句將newed物件儲存於(置入)智慧指標內。如果不這樣做,一旦異常丟擲,有可能導致難以察覺的資源洩漏。