標籤:
章節回顧:
《Effective C++》第1章 讓自己習慣C++-讀書筆記
《Effective C++》第2章 構造/析構/賦值運算(1)-讀書筆記
《Effective C++》第2章 構造/析構/賦值運算(2)-讀書筆記
《Effective C++》第3章 資源管理(1)-讀書筆記
《Effective C++》第3章 資源管理(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()); //絕不會造成泄露
請記住:以獨立語句將newdObject Storage Service於智能指標內。如果不這樣做,一旦異常被拋出,有可能造成難以察覺的資源流失。
《Effective C++》第3章 資源管理(2)-讀書筆記