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物件儲存於(置入)智慧指標內。如果不這樣做,一旦異常丟擲,有可能導致難以察覺的資源洩漏。