章節回顧:
《Effective C++》第1章 讓自己習慣C++-讀書筆記
《Effective C++》第2章 構造/析構/賦值運算(1)-讀書筆記
《Effective C++》第2章 構造/析構/賦值運算(2)-讀書筆記
《Effective C++》第3章 資源管理(1)-讀書筆記
《Effective C++》第3章 資源管理(2)-讀書筆記
《Effective C++》第4章 設計與宣告(1)-讀書筆記
《Effective C++》第4章 設計與宣告(2)-讀書筆記
《Effective C++》第8章 定製new和delete-讀書筆記
條款15:在資源管理類中提供對原始資源的訪問
許多API直接指涉資源,所以除非你永遠不用它們,否則都會繞過資源管理物件直接訪問原始資源。假設使用tr1::shared_ptr管理物件。
std::tr1::shared_ptr<Investment> pInv(createInvestment());
函式daysHeld的宣告是這樣的:
int daysHeld(const Investment *pi);
下面這種呼叫方式,肯定是錯誤的:
int days = daysHeld(pInv); //錯誤
因為函式需要的是指標,你傳遞是一個tr1::shared_ptr<Investment>物件。所以你需要一個函式將RAII物件轉換為所內含的原始資源。有兩種方法:隱式轉換和顯示轉換。
(1)顯示轉換
tr1::shared_ptr和auto_ptr都提供了一個成員函式get返回內部的原始指標,這是顯式轉換。
int days = daysHeld(pInv.get()); //好的,沒有問題
(2)隱式轉換
tr1::shared_ptr和auto_ptr都過載了操作符operator->和operator*,這樣就允許隱式轉換到原始指標。舉例:假設Investment類有個成員函式bool isTaxFree() const;那麼下面的呼叫是OK的:
bool taxable1 = !(pInv->isTaxFree()); //好的,沒有問題 bool taxable2 = !((*pInv).isTaxFree()); //好的,沒有問題
現在的問題是,需要原始指標的地方(例如,函式形參),如何以智慧指標代替。解決方法是:提供一個隱式轉換函式。下面舉個字型類的例子:
FontHandle getFont(); //取得字型控制程式碼 void releaseFont(FontHandle fh); //釋放控制程式碼 class Font { public: explicit Font(FontHandle fh) : f(fh){} ~Font() { releaseFont(f); } private: FontHandle f; };
如果C API處理的是FontHandle而不是Font物件,當然你可以像tr1::shared_ptr和auto_ptr那樣提供一個get()函式:
FontHandle get() const { return f; } //顯示轉換函式
這樣是可以的,但客戶還是覺得麻煩,這時候定義一個隱式轉換函式是必須的。
class Font { public: ... operator FontHandle() const { return f; } ... };
注意:假設你已經知道了隱式轉換函式的用法。例如:必須定義為成員函式,不允許轉換為陣列和函式型別等。
完成了以上工作,對於下面這個函式的呼叫是OK的:
void changeFontSize(FontHandle f, int newSize); Font f(getFont()); int newFontSize; changeFontSize(f, newFontSize); //好的,Font隱式轉換為FontHandle了。
隱式型別轉換也增加了一種風險。例如有以下程式碼:
Font f1(getFont()); FontHandle f2 = f1; //將Font錯寫成FontHandle了,編譯仍然通過。
f1被隱式轉換為FontHandle,這時f1和f2共同管理某個資源,f1被銷燬,字型釋放,這時候你可以想象f2的狀態(原諒我這個詞我不會說),再銷燬f2,必然會造成執行錯誤。通常提供一個顯示轉換get函式是比較好的,因為它可以避免非故意的型別轉換的錯誤,這種錯誤估計會耗費你很長的除錯時間(我遇到過的情況)。
請記住:
(1)有些API要求訪問原始資源,所以每一個RAII class應該提供一個“取得其所管理的資源”的辦法。
(2)對原始資源的訪問可以通過顯式轉換或隱式轉換。一般顯式轉換比較安全,但隱式轉換對客戶比較方便。
條款16:成對使用new和delete時要採取相同形式
我相信你肯定一眼看出以下程式碼的問題:
std::string *stringArray = new std::string[100]; delete stringArray;
程式行為是未定義的。stringArray所含的100個string物件,99個可能沒被刪除,因為它們的解構函式沒被呼叫。
delete必須要知道的是:刪除的記憶體有多少個物件,決定了呼叫多少個解構函式。
單一物件和陣列的記憶體佈局肯定是不同的,陣列佔用的記憶體也許包含一個“陣列大小”的記錄。(編譯器乾的事)你可以告訴編譯器刪除的是陣列還是單一物件:
std::string *stringPtr1 = new std::string; std::string *stringPtr2 = new std::string[100]; delete stringPtr1; //刪除一個物件 delete [] stringPtr2; //刪除一個陣列
這個規則很簡單,但有一點需要注意:
typedef std::string AddressLines[4]; std::string *pal = new AddressLines; delete pal; //不好,行為未定義。 delete [] pal; //很好。
所以,最好不要對陣列做typedef。
請記住:如果new表示式中使用了[],必須在相應的delete表示式中使用[];如果new表示式中不使用[],一定不要在相應的delete表示式中使用[]。
條款17:以獨立語句將newed物件置入智慧指標
假設有以下函式,具體含義我們可以先忽略:
int priority(); void processWidget(std::tr1::shared_ptr<Widget> pw, int priority);
先說明一點的是,當你以下面形式呼叫processWidget時,肯定是錯誤的:
processWidget(new Widget, priority());
編譯出錯。tr1::shared_ptr建構函式需要一個原始指標,但該建構函式是explicit,即禁止隱式型別轉換的。所以,正常情況你應該這樣呼叫:
processWidget(std::tr1::shared_ptr<Widget>(new Widget), priority()); //好的,沒有問題
以這種呼叫方式,執行函式體之前,有三個工作要做:
(1)呼叫priority。
(2)執行"new Widget"。
(3)呼叫tr1::shared_ptr建構函式
可以肯定的是(2)在(3)前面執行,但(1)的執行次序不能確定。假設(1)在第二個被執行,則如果呼叫priority出現異常,new Widget返回的指標將會遺失,因為還未執行tr1::shared_ptr的建構函式。所以,發生了資源洩露。
避免這類問題很簡單:使用分離語句。
std::tr1::shared_ptr<Widget> pw(new Widget); processWidget(pw, priority()); //絕不會造成洩露
請記住:以獨立語句將newd物件儲存於智慧指標內。如果不這樣做,一旦異常被丟擲,有可能造成難以察覺的資源洩露。