淺談shared_ptr與循環引用問題

前面對於智能指針已經大致的提過了(http://blog.csdn.net/chenkaixin_1024/article/details/69276679),但是關於shared_ptr上一篇文章沒有仔細分析,這裏把它單獨拎出來理一理。

前面已經提過由於scoped_ptr(unique_ptr)無法解決讓多個對象管理同一塊空間的情況,標準庫中又追加引用了shared_ptr,而我們的shared_ptr是參考了string類當中的引用計數這個巧妙的方法來解決多個對象管理同一塊空間的問題的,而且除此之外我們這裏的模擬實現還要考慮跟多一點,就是加入刪除器的部分,以適用不同的指針。到這裏如果有不清楚可以參考上一篇博客,鏈接就在文章開頭。

我在這篇博客想要重點討論一下shared_ptr所存在的循環引用的問題:

#include <iostream>  
using namespace std;  
  
#include <memory>  
#include "SharedPtr.h"  
  
struct Node  
{  
    Node(int va)  
        :value(va)  
          
    {  
        cout<<"Node()"<<endl;  
    }  
  
  
    ~Node()  
    {  
        cout<<"~Node()"<<endl;  
    }  
    shared_ptr<Node> _pre;  
    shared_ptr<Node> _next;  
    int value;  
};  
  
void funtest()  
{  
    shared_ptr<Node> sp1(new Node(1));  
    shared_ptr<Node> sp2(new Node(2));  
  
    sp1->_next=sp2;  
    sp2->_pre=sp1;  
}  
int main()  
{  
    funtest();  
    return 0;  
}  

上述程序在執行完sp2->_pre=sp1;這條語句之後,也就是在構建完兩個節點相互指向之後,大致模型如圖:


shared_ptr的對象模型大致分爲指向引用計數空間的指針_Ref和指向節點空間的_ptr,而我們引用計數空間在標準庫中其實有兩個long類型的計數值use和weak,而且在空間被創建成功後,初始化都給成1。

由於一塊空間被另外一個shared_ptr的對象所管理,只會增加引用計數中use的值,至於weak的值,後面再說。

根據上面的測試用例,我們知道sp1,sp2,_pre,_next均是shared_ptr的對象,所以上圖中兩個引用計數空間中的use均爲2,weak均爲1,而在出funtest()的作用域之前,會對棧空間上的變量進行銷燬釋放,也就是說在這裏,會對sp1和sp2這兩個對象進行釋放,調用它們的析構函數,但由於在shared_ptr的析構函數中,只有當use=1,進行減減之後爲0,纔會釋放_ptr所指向的空間,所以在這裏sp1和sp2所管理的節點空間是不會被釋放的,因此也不會調用~Node()這個析構函數,所以這裏就出現了上篇文章末尾所出現的問題,也就是內存泄漏。

由於在shared_ptr單獨使用的時候會出現循環引用的問題,造成內存泄漏,所以標準庫又從boost庫當中引入了weak_ptr。

對上面的測試用例進行修改:

#include <iostream>  
using namespace std;  
  
#include <memory>  
#include "SharedPtr.h"  
  
struct Node  
{  
    Node(int va)  
        :value(va)  
          
    {  
        cout<<"Node()"<<endl;  
    }  
  
  
    ~Node()  
    {  
        cout<<"~Node()"<<endl;  
    }  
    weak_ptr<Node> _pre;  
    weak_ptr<Node> _next;  
    int value;  
};  
  
void funtest()  
{  
    shared_ptr<Node> sp1(new Node(1));  
    shared_ptr<Node> sp2(new Node(2));  
  
    sp1->_next=sp2;  
    sp2->_pre=sp1;  
}  
int main()  
{  
    funtest();  
    return 0;  
}  
其實也並沒有做太大的修改,只是將其中的_pre和_next的類型換成weak_ptr,當這個測試用例運行到sp2->_pre=sp1結束後,節點之間的相互關係與上圖基本無差別,但是這裏有一點發生了變化,就是由於每個節點僅有一個shared_ptr的對象管理,這裏兩塊引用計數空間中的use均爲1,而由於多了weak_ptr的對象_pre或_next指向,這裏weak的值就變成了2,因此根據前面的討論,兩個節點空間就成功釋放了:


而這裏關於這個空間到底是如何釋放的以及對於引用計數的空間的釋放還要進一步討論:

下面跟着測試用例逐步調試空間的釋放過程(大致如下):



首先,先對sp2這一share_ptr對象進行釋放,調用其析構函數(即第1步),緊跟着向後走,走到第3步,先對當前use進行減減,並判斷是否爲0,這裏由於use之前爲1,這裏剛好就等於0;所以進入if語句內,調用_Destroy(),釋放_ptr所管理的資源(這裏即爲節點),所以調用~Node(),對sp2所管理的節點進行析構,這裏注意的是節點的結構中有weak_ptr類型的對象_pre和_next;而這裏sp2->_pre=sp1,所以要對_pre進行析構必然要調用其析構函數,調用過程大致如下:

在第8步,對sp1的weak進行減減(注意:這裏是對sp1對應的引用計數中的weak減減,使其爲1)嘗試釋放sp1對應的引用計數空間,但由於weak在這裏爲1,所以對sp1的引用計數空間並未能釋放(未能進行第9步調用);
在對sp2所管理的節點釋放之後,就完成了之前第4步的調用,接下來進行第5步調用_Decwref(),嘗試釋放sp2所對應的引用計數空間,但由於這裏weak等於2,- -weak之後,weak=1,所以並不能釋放對應引用計數空間,到這裏爲止,就完成對sp2對象的釋放;

隨後進行sp1的釋放,大致與sp2的釋放相同,但是在執行第8步的時候,由於之前已經對sp2的weak減減過一次了,這裏再減減就等於0,進行對sp2的引用計數空間的釋放;隨後在第5步的操作時,由於之前已經對sp1的weak減減過一次了,這裏再減減就等於0,進行對sp1的引用計數空間的釋放;而對於sp1所管理的節點,與sp2的情況相同在第4步進行了釋放。到此爲止,所有的空間都得到了釋放(包括引用計數空間),因此這裏就不會發生內存泄漏。

從頭到尾過程大致如下:





發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章