std::shared_ptr 和 std::weak_ptr的用法以及引用計數的循環引用問題


在std::shared_ptr被引入之前,C++標准庫中實現的用於管理資源的智能指針只有std::auto_ptr一個而已。std::auto_ptr的作用非常有限,因為它存在被管理資源的所有權轉移問題。這導致多個std::auto_ptr類型的局部變量不能共享同一個資源,這個問題是非常嚴重的哦。因為,我個人覺得,智能指針內存管理要解決的根本問題是:一個堆對象(或則資源,比如文件句柄)在被多個對象引用的情況下,何時釋放資源的問題。何時釋放很簡單,就是在最后一個引用它的對象被釋放的時候釋放它。關鍵的問題在於無法確定哪個引用它的對象是被最后釋放的。std::shared_ptr確定最后一個引用它的對象何時被釋放的基本想法是:對被管理的資源進行引用計數,當一個shared_ptr對象要共享這個資源的時候,該資源的引用計數加1,當這個對象生命期結束的時候,再把該引用技術減少1。這樣當最后一個引用它的對象被釋放的時候,資源的引用計數減少到0,此時釋放該資源。下邊是一個shared_ptr的用法例子:

 

[cpp]  view plain  copy
 
 print?
  1. #include <iostream>  
  2. #include <memory>  
  3.   
  4. class Woman;  
  5. class Man{  
  6. private:  
  7.     std::weak_ptr<Woman> _wife;  
  8.     //std::shared_ptr<Woman> _wife;  
  9. public:  
  10.     void setWife(std::shared_ptr<Woman> woman){  
  11.         _wife = woman;  
  12.     }  
  13.   
  14.     void doSomthing(){  
  15.         if(_wife.lock()){  
  16.         }  
  17.     }  
  18.   
  19.     ~Man(){  
  20.         std::cout << "kill man\n";  
  21.     }  
  22. };  
  23.   
  24. class Woman{  
  25. private:  
  26.     //std::weak_ptr<Man> _husband;  
  27.     std::shared_ptr<Man> _husband;  
  28. public:  
  29.     void setHusband(std::shared_ptr<Man> man){  
  30.         _husband = man;  
  31.     }  
  32.     ~Woman(){  
  33.         std::cout <<"kill woman\n";  
  34.     }  
  35. };  
  36.   
  37.   
  38. int main(int argc, char** argv){  
  39.     std::shared_ptr<Man> m(new Man());  
  40.     std::shared_ptr<Woman> w(new Woman());  
  41.     if(m && w) {  
  42.         m->setWife(w);  
  43.         w->setHusband(m);  
  44.     }  
  45.     return 0;  
  46. }  

    在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環路。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM