《Effective C++》第3章 資源管理(2)-讀書筆記

QingLiXueShi發表於2015-04-23

章節回顧:

《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++》第5章 實現-讀書筆記

《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物件儲存於智慧指標內。如果不這樣做,一旦異常被丟擲,有可能造成難以察覺的資源洩露。

相關文章