章節回顧:
《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-讀書筆記
記憶體只是你必須管理的眾多資源之一。其他常見的資源還包括檔案描述器(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、使用引用計數。其他行為也可能被實現。