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

QingLiXueShi發表於2015-04-22

章節回顧:

《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-讀書筆記


 

記憶體只是你必須管理的眾多資源之一。其他常見的資源還包括檔案描述器(file descriptors)、互斥鎖(mutex locks)、圖形介面中的字型和筆刷、以及網路sockets。無論哪一種資源,重要的是當你不再使用它時,將它還給系統。

條款13:以物件管理資源

下面有一個Investment繼承體系:

class Investment { ... };

Investment是root class,獲得Investment體系的物件是通過工廠函式:

Investment* createInvestment(); //返回指標指向Investment繼承體系動態分配物件

顯然,createInvestment的呼叫者使用後,有責任刪除返回的指標,以釋放物件。假設函式f()負責這個行為:

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

但至少存在幾種情況使得f()無法刪除createInvestment物件:

(1)…區域有一個過早的return語句,導致delete沒執行。

(2)…區域丟擲異常,控制流不會轉向delete。

當delete時,我們洩露的不只是內含Investment物件的那塊記憶體,還包括其所儲存的任何資源。

 

為確保createInvestment()返回的資源總是被釋放,我們需要將資源放進物件內,當控制流離開f()時,該物件的解構函式會自動釋放那些資源。標準庫提供的auto_ptr是個“類指標物件”,即智慧指標,其解構函式自動對其所指物件呼叫delete。

下面是f()的修正版:

void f()
{
    std::auto_ptr<Investment> pInv(createInvestment());            //由auto_ptr的解構函式自動刪除pInv
    ...
}

f()說明了以物件管理資源的兩個重要方面:

(1)獲得資源後立刻放進管理物件內。

以資源管理物件的觀念常被稱為“資源取得時機便是初始化時機”(Resource Acquisition Is Initialization, RAII)。每一筆資源在獲得的同時立刻被放進管理物件中。

(2)管理物件運用解構函式確保資源被釋放。

無論控制流如何離開f(),一旦物件離開作用域(物件被銷燬),其解構函式被呼叫,資源被釋放。

注意:由於auto_ptr被銷燬時會自動刪除它所指之物,所以別讓多個auto_ptr同時指向某一物件。為了預防這個問題auto_ptr有一個很好的性質:若通過copy建構函式或copy assignment操作符複製它們,它們會變成null,而複製所得的指標將取得資源的唯一權。

std::auto_ptr<Investment> pInv1(createInvestment());
std::auto_ptr<Investment> pInv2(pInv1);            //pInv2指向物件,pInv1被設為null
pInv1 = pInv2;                                    //pInv1指向物件,pInv2被設為null

正是因為auto_ptr物件具備“非正常”的拷貝性質,所以不能用於STL容器的元素等。

 

auto_ptr的替代方案是:引用計數型智慧指標(reference-counting smart pointer (RCSP))。RCSP能夠追蹤共有多少物件指向某種資源,並在無人指向它時自動刪除該資源。RCSP提供的行為類似垃圾回收,不同的是它無法打破環狀引用,例如,兩個其實已經沒被使用的物件彼此互指,好像物件還處於“被使用”狀態。

TR1的tr1::shared_ptr就是一個RCSP,f()的新寫法:

void f()
{
    std::tr1::shared_ptr<Investment> pInv(createInvestment());
...
}

f()的新版本與使用auto_ptr幾乎相同,但拷貝行為正常了:

void f()
{
    ...
    std::tr1::shared_ptr<Investment> pInv1(createInvestment());
    std::tr1::shared_ptr<Investment> pInv2(pInv1);            //pInv1和pInv2指向同一物件
    pInv1 = pInv2;                                            //pInv1和pInv2指向同一物件
    ...
}

因為shared_ptr的複製行為是正常的,所以可用於STL容器以及其他auto_ptr非正常複製行為並不適用的情況。

特別注意:auto_ptr和shared_ptr兩者都在解構函式內做delete而不是delete[]。所以下面的操作是非常錯誤的:

std::auto_ptr<std::string> aps(new std::string[10]);
std::tr1::shared_ptr<int> spi(new int[1024]);

之所以沒有特別針對C++動態分配陣列而設計的類似auto_ptr或tr1::shared_ptr是因為vector和string總是可以取代動態陣列,這是你的第一選擇。

請記住:

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

(2)兩個常被使用的RAII class分別是:tr1::shared_ptr和auto_ptr。前者是最好的選擇,因為其copy行為比較正常。auto_ptr的copy會使被複制物指向null。


 

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

並非所有資源都是heap-based的,對這種資源auto_ptr和tr1::shared_ptr這樣的智慧指標往往不適用。舉例如下:

有兩個函式用來處理Mutex互斥器物件:

void lock(Mutex *pm);            //鎖定pm指向的互斥器
void unlock(Mutex *pm);            //解鎖

為確保不會忘記解鎖,可以建立一個class管理鎖,這個class基本結構由RAII支配,即“資源在構造期間獲得,在析構期間釋放”。

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

客戶是這樣使用的:

Mutex m;                    //定義所需的互斥器
{
...
    Lock ml(&m);           //鎖定互斥器
...
}                            //自動解鎖

這樣使用是可以的。但如果發生copy的情況呢?即當一個RAII物件被複制,會發生什麼事情?

Lock ml1(&m);
Lock ml2(ml1);

有四種可能:

(1)禁止複製。

許多時候允許RAII物件被複制並不合理。

(2)對底層資源使用“引用計數法”。

有時候我們希望儲存資源,直到它的最後一個使用者被銷燬。tr1::shared_ptr便是如此處理。

注意:tr1::shared_ptr的預設行為是當引用次數為0時刪除其所指物,我們想要的是釋放鎖,而不是刪除。我們可以指定“刪除器”,這是一個函式,當引用次數為0時會被呼叫。auto_ptr無此性質,它總是刪除它的指標。

更改後的版本是:

class Lock
{
public:
    explicit Lock(Mutex *pm) : mutexPtr(pm, unlock)        //unlock為刪除器
    {
        lock(mutexPtr.get());
    }
private: 
    std::tr1::shared_ptr<Mutex> mutexPtr;            //使用shared_ptr替換raw_pointer
};

注意:這個lock class不需要解構函式了,因為編譯器會自動呼叫mutexPtr的解構函式,當引用計數為0時,自動呼叫刪除器。

(3)複製底部資源

可以針對一份資源擁有任意數量的副本。這是的複製是深拷貝。

(4)轉移底部資源的擁有權

你可能希望永遠只有一個RAII物件指向一個raw recource,即使RAII物件被複制。這是資源的擁有權會從被複制物轉移到目標物。auto_ptr便是如此。

注意:copying函式(包括copy建構函式和copy assignment操作符)有可能被編譯器創造出來,因此除非編譯器版本做了你想做的事,否則你要自己編寫它們。

請記住:

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

(2)普遍常見的RAII class copying行為是:禁止copying、使用引用計數。其他行為也可能被實現。

相關文章