在std::shared_ptr被引入之前,C++標准庫中實現的用於管理資源的智能指針只有std::auto_ptr一個而已。std::auto_ptr的作用非常有限,因為它存在被管理資源的所有權轉移問題。這導致多個std::auto_ptr類型的局部變量不能共享同一個資源,這個問題是非常嚴重的哦。因為,我個人覺得,智能指針內存管理要解決的根本問題是:一個堆對象(或則資源,比如文件句柄)在被多個對象引用的情況下,何時釋放資源的問題。何時釋放很簡單,就是在最后一個引用它的對象被釋放的時候釋放它。關鍵的問題在於無法確定哪個引用它的對象是被最后釋放的。std::shared_ptr確定最后一個引用它的對象何時被釋放的基本想法是:對被管理的資源進行引用計數,當一個shared_ptr對象要共享這個資源的時候,該資源的引用計數加1,當這個對象生命期結束的時候,再把該引用技術減少1。這樣當最后一個引用它的對象被釋放的時候,資源的引用計數減少到0,此時釋放該資源。下邊是一個shared_ptr的用法例子:
- #include <iostream>
- #include <memory>
- class Woman;
- class Man{
- private:
- std::weak_ptr<Woman> _wife;
- //std::shared_ptr<Woman> _wife;
- public:
- void setWife(std::shared_ptr<Woman> woman){
- _wife = woman;
- }
- void doSomthing(){
- if(_wife.lock()){
- }
- }
- ~Man(){
- std::cout << "kill man\n";
- }
- };
- class Woman{
- private:
- //std::weak_ptr<Man> _husband;
- std::shared_ptr<Man> _husband;
- public:
- void setHusband(std::shared_ptr<Man> man){
- _husband = man;
- }
- ~Woman(){
- std::cout <<"kill woman\n";
- }
- };
- int main(int argc, char** argv){
- std::shared_ptr<Man> m(new Man());
- std::shared_ptr<Woman> w(new Woman());
- if(m && w) {
- m->setWife(w);
- w->setHusband(m);
- }
- return 0;
- }
在Man類內部會引用一個Woman,Woman類內部也引用一個Man。當一個man和一個woman是夫妻的時候,他們直接就存在了相互引用問題。man內部有個用於管理wife生命期的shared_ptr變量,也就是說wife必定是在husband去世之后才能去世。同樣的,woman內部也有一個管理husband生命期的shared_ptr變量,也就是說husband必須在wife去世之后才能去世。這就是循環引用存在的問題:husband的生命期由wife的生命期決定,wife的生命期由husband的生命期決定,最后兩人都死不掉,違反了自然規律,導致了內存泄漏。
解決std::shared_ptr循環引用問題的鑰匙在weak_ptr手上。weak_ptr對象引用資源時不會增加引用計數,但是它能夠通過lock()方法來判斷它所管理的資源是否被釋放。另外很自然地一個問題是:既然weak_ptr不增加資源的引用計數,那么在使用weak_ptr對象的時候,資源被突然釋放了怎么辦呢?呵呵,答案是你根本不能直接通過weak_ptr來訪問資源。那么如何通過weak_ptr來間接訪問資源呢?答案是:在需要訪問資源的時候weak_ptr為你生成一個shared_ptr,shared_ptr能夠保證在shared_ptr沒有被釋放之前,其所管理的資源是不會被釋放的。創建shared_ptr的方法就是lock()方法。
細節:shared_ptr實現了operator bool() const方法來判斷一個管理的資源是否被釋放。
條款20:使用std::weak_ptr作為一個類似std::share_ptr但卻能懸浮的指針
有一個矛盾,一個靈巧指針可以像std::shared_ptr (見條款 19)一樣方便,但又不參與管理被指對象的所有權。換句話說,需要一個像std::shared_ptr但又不影響對象引用計數的指針。這類指針會有一個std::shared_ptr沒有的問題:被指的對象有可能已經被銷毀。一個良好的靈巧指針應該能處理這種情況,通過跟蹤什么時候指針會懸浮,比如在被指對象不復存在的時候。這正是std::weak_ptr這類型靈巧指針所能做到的。
你可能疑惑std::weak_ptr能有什么用處,在你看了std::weak_ptr的API后可能更疑惑。它看上去根本不靈巧。std::weak_ptr不能解引用,也不能檢查是否為空。這是因為std::weak_ptr不能作為一個獨立的靈巧指針,它是作為std::shared_ptr的延伸。
指針生成的時刻就決定了這種關系。std::weak_ptr一般是通過std::shared_ptr來構造的。當std::shared_ptr初始化std::weak_ptr時,std::weak_ptr就指向了相同的地方,但它不改變所指對象的引用計數。
auto spw = // after spw is constructed,
std::make_shared<Widget>(); // the pointed-to Widget's
// ref count (RC) is 1. (See
// Item 21 for info on
// std::make_shared.)
…
std::weak_ptr<Widget> wpw(spw); // wpw points to same Widget
// as spw. RC remains 1
…
spw = nullptr; // RC goes to 0, and the
// Widget is destroyed.
// wpw now dangles
std::weak_ptr成為懸浮指針也被稱作過期。你可以直接檢測,
if (wpw.expired()) … // if wpw doesn't point
// to an object…
但是經常期望的是檢查一個std::weak_ptr是否已經過期,以及是否不能訪問訪問做指向的對象。這個比較難做到。因為std::weak_ptr缺乏解引用操作,沒法寫這樣的代碼。即使有,把檢查和解引用分隔開來也會引起競爭沖突:在調用過期操作(expired)和解引用之間。另一個線程會重新分配或者刪除指向對象的最后一個std::shared_ptr,這會引起的對象被銷毀,於是你的解引用會產生未定義行為。
你所需要的是一個原子操作來檢查std::weak_ptr是否過期,如果沒過期則提供對所指對象的訪問。可以通過從std::weak_ptr構造std::shared_ptr來實現上述操作。這個操作有兩個形式,取決於假如你從std::weak_ptr來構造std::shared_ptr時std::weak_ptr已經失效你期望發生什么情況。一種形式是std::weak_ptr::lock,它返回一個std::shared_ptr。如果std::weak_ptr失效,則std::shared_ptr為空:
std::shared_ptr<Widget> spw1 = wpw.lock(); // if wpw's expired,
// spw1 is null
auto spw2 = wpw.lock(); // same as above,
// but uses auto
另一種形式是把std::weak_ptr作為參數來構造std::shared_ptr。這樣,如果std::weak_ptr失效的話,則會拋異常:
std::shared_ptr<Widget> spw3(wpw); // if wpw's expired,
// throw std::bad_weak_ptr
可能你還是很疑惑std::weak_ptr怎樣使用呢。設想一個工廠函數,基於唯一ID來創建一些指向只讀對象的靈巧指針。根據條款18對工廠函數返回類型的建議,應該返回一個 std::unique_ptr:
std::unique_ptr<const Widget> loadWidget(WidgetID id);
假如loadWidget是一個昂貴的調用(比如因為涉及到文件或數據庫io)而且經常會被相同的ID重復調用,一個合理的優化是寫一個函數做loadWidget的工作,並且緩存結果。然而保持每一個請求過的Widget在緩存中可能會引起性能問題,所以另一個優化就是在Widget不再使用時刪除之。
對這個工廠函數來說,返回一個std::unique_ptr並不是最合適的。調用者獲得靈巧指針並緩存下來,調用者決定了這些對象的生存期,但是緩存也需要一個指向這些對象的指針。緩存的指針需要能夠檢測什么時候是懸浮的,因為工廠函數的使用者不在使用這些指針時,對象會被銷毀,這樣相關的cache項就會變成懸浮指針。於是緩存的指針應該是std::weak_ptr---這樣可以檢測到什么時候懸浮。那么這意味着工廠的返回值應該是std::shared_ptr,因為只有對象的生存期由std::shared_ptr管理時,std::weak_ptr才可以檢測到何時懸浮。
這里有個快速但不好的loadWidget緩存實現方案:
std::shared_ptr<const Widget> fastLoadWidget(WidgetID id)
{
static std::unordered_map<WidgetID,
std::weak_ptr<const Widget>> cache;
auto objPtr = cache[id].lock(); // objPtr is std::shared_ptr
// to cached object (or null
// if object's not in cache)
if (!objPtr) { // if not in cache,
objPtr = loadWidget(id); // load it
cache[id] = objPtr; // cache it
}
return objPtr;
}
這個實現使用了C++11中的hash table容器(std::unorderer_map),盡管沒有顯示出WidgetID的hash計算和比較相等的函數。
fastLoadWidget 的實現忽略了緩存中的過期的std::weak_ptr會不斷積累,因為相關聯的Widget可能不再使用(因此會被銷毀)。這個實現可以被改善,而不是花時間去深入到std::weak_ptr中去觀察,讓我們考慮第二種情況:觀察者模式。改模式的主要部件是主題(Subjects,狀態可以改變的對象)和觀察者(Observers,狀態改變發生后被通知的對象)。多數實現中,每個subject包含了一個數據成員,保持着指向observer的指針,這樣很容易在subject發生狀態改變時發通知。subject沒有興趣控制它們的observer的生命周期(不關心何時它們被銷毀),但他要確認一個observer是否被銷毀,這樣避免去訪問。一個合理的設計就是每個subject保存一個容器,容器里放着每個observer的std::weak_ptr,這樣在subject在使用前就可以檢查observer指針是否是懸浮的。
最后再舉一個使用std::weak_ptr的例子,考慮一個數據結構,里面包含了A,B,C3個對象,A和C共享B的所有權,因此保持了一個B的std::shared_ptr:
假設有一個指針從B回指向A,那么這個指針應該用什么類型的指針呢?
有三中選擇:
1.一個原始指針。 這種情況下,如果A被銷毀,C依然指向B,B保存着指向A的指針,但是已經是懸浮指針了。B卻檢測不出來,所以B有可能去解引用這個懸浮指針,結果就是為定義的行為。
2.一個std::shared_ptr。這種情況下,A和B互相保存着一個std::shared_ptr,結果這個環路(A指向B,B指向A)組織了A和B被銷毀。即使程序的其他數據已經不再訪問A和B,它們兩者都互相保存着對方一個引用計數。這樣,A和B就內存泄漏了,實用中,程序將不可能訪問到A和B,而它們的資源也將不會被重新使用。
3.一個std::weak_ptr。這將避免上述兩個問題。假如A被銷毀,那么B的回指指針將會懸浮,但是B可以檢測到。進一步說,A和B雖然都互相指想彼此,但是B的指針不影響A的引用計數,所以當std::shared_ptr不再指向A時,並不能阻止A被銷毀。
使用std::weak_ptr無疑是最好的選擇。然而用std::weak_ptr來打破std::shared_ptr引起的循環並不那么常見,所以這個方法也不值一提。嚴格來講層級數據結構,比如tree,孩子結點一般都只被父節點擁有,當父節點被銷毀后,所有的孩子結點也都應該被銷毀。這樣,從父節點到子節點的鏈接可以用std::unique_ptr來表示,而反過來從子節點到父節點的指針可以用原始指針來實現,因為子節點的生命周期不會比父節點的更長,所以不會出現子節點去解引用一個父節點的懸浮指針的情況。
當然並非所有的基於指針的數據結構都是嚴格的層級關系的。比如像緩存的情況以及觀察者列表的實現,使用std::weak_ptr就非常好。
從效率的角度來看,std::weak_ptr和std::shared_ptr幾乎一致。它們尺寸相同,都使用了控制塊(見條款19),其構造,析構,賦值都涉及了對引用計數的原子操作。你可能會吃驚,因為我在本條款開始提到了std::weak_ptr不參與引用計數的操作。其實那不是我寫的,我寫的是std::weak_ptr不涉及對象的共享所有權,因此不影響對象的引用計數。實際山控制塊里面有第二個引用計數,std::weak_ptr操作的就是這第二個引用計數。更詳細的描述見條款21。
需要記住的事情
1.使用std::weak_ptr來指向可能懸浮的std::shared_ptr一樣的指針。
2.可能使用std::weak_ptr的情況包括緩存,觀察模式中的觀察者列表,以及防止std::shared_ptr環路。