智能指針與弱指針解決循環引用

       當我們學會使用智能指針的時候,會發現他有很多好處,但庫裏面提供的智能指針shared_ptr也並不是萬能的,他會存在循環引用的問題,下面我們就通過實例來具體分析循環引用這種情況。
struct  Node
{
	Node( const T&data)
		:_data(data)
		, _Pre(NULL) 
	,_Pnext(NULL)
	{
	}
	shared_ptr<Node<T>> _Pre;
	shared_ptr<Node<T>> _Pnext;
	T _data;
	~Node()
	{
		cout << "~Node()" << endl;
	}
};
void FunTest()
{
	    shared_ptr<Node<int>> sp1(new Node<int>(10));
    shared_ptr<Node<int>> sp2(new Node<int>(20));
cout << sp1.use_count() << endl;
cout << sp2.use_count() << endl;


sp1->_Pnext = sp2;
sp2->_Pre = sp1;
cout << sp1.use_count() << endl;
cout << sp2.use_count() << endl;
}
int main()
{
	FunTest();
	getchar();
	return 0;
}
       在這個例子中,我們自定義了一個節點類型Node,它包含兩個智能指針,一個_Pnext指向下一個節點,另一個_Pre指向前一個節點,還包含值域data,並且給出了它的構造函數和析構函數,在析構函數中打印析構函數名,以測試是否調用,在FunTest()函數中又通過智能指針創建了兩個節點對象sp1和sp2.然後分別打印sp1和sp2的引用計數,在這裏下一個斷點,觀察程序運行結果。

接下來,把兩個節點連接起來。再打印它們的引用計數。
出了FunTest函數的作用域,兩個對象應該被銷燬,來看看程序 運行結果。

並沒有打印出析構函數名,這就說明析構函數沒有被調用,這就存在內存泄漏,而這種情況就是因爲循環引用造成的,爲了解釋循環引用的原理,我們先分析shared_ptr的構造原理。
我們通過跟進庫函數一步一步得知它的結構如下。
      利用上圖我們來理解shaerd_ptr的結構,它首先公有繼承自一個基類_Ptr_base,這個基類包含了兩個成員,一個是T*_Ptr,另外一個是_Ref_count_base類型的指針_Rep,通過查看定義我們查看了_Ref_count_base這個基類的結構,他是一個抽象類,它有兩個成員_Uses和_Weaks,不難發現,他們就是引用計數,並且在構造這個基類時這倆引用計數都置爲1。
        並且在庫裏面有三個類繼承自這個基類。_Ref_count只有一個_Ptr,他有兩個銷燬函數_Dsetroy()和_Delete_this(),_Ref_count_del是帶定製刪除器的引用計數類,它銷燬空間時需要調用定製刪器即可。而_Ref_count_del_alloc是帶有刪除器和空間配置器的引用計數類。_Ptr最後指向節點空間,_Ref_count_base*Ref這個基類指針最後指向引用計數的對象。
下面是sp1的構造過程:
1.申請出節點空間。
2.調用shared_ptr的構造函數。
3.構造基類_Ptr_base,使_Ptr指向節點空間,new出引用計數_Ref_count。
4.構造引用計數,先構造引用計數的基類使_Uses和_Weaks爲1。
5.構造引用計數_Ref_count的_ptr使其指向節點空間。
當我們將兩個節點連接起來之後,sp1和sp2的引用計數_Uses都將增加爲2,出了函數的作用域,先銷燬sp2,將它的引用計數減爲1,不爲0;變量銷燬,而節點空間並沒有釋放。同樣的銷燬sp1時,引用計數減爲1不爲0,銷燬變量,空間沒有釋放。這兩個對象相互咬着誰都不肯放。這就是循環引用。怎麼解決這個問題呢,我們採用的是weak_ptr,注意:weak_ptr不能單獨使用管理空間,我們將代碼重寫如下。
#include<iostream>
#include<memory>
using namespace std;
template<class T>
struct  Node
{
	Node(const T&data)
		:_data(data)
	{
	}
	weak_ptr<Node<T>> _Pre;
	weak_ptr<Node<T>> _Pnext;
	T _data;
	~Node()
	{
		cout << "~Node():" <<this<< endl;
	}
};
void FunTest()
{
	    shared_ptr<Node<int>> sp1(new Node<int>(10));
    shared_ptr<Node<int>> sp2(new Node<int>(20));
cout << sp1.use_count() << endl;
cout << sp2.use_count() << endl;


sp1->_Pnext = sp2;
sp2->_Pre = sp1;
cout << sp1.use_count() << endl;
cout << sp2.use_count() << endl;
}
int main()
{
	FunTest();
	getchar();
	return 0;
}
這次我們觀察運行結果。

兩個節點成功的被銷燬,下面我們來看看weak_ptr的構成以及原理。

1.首先weak_ptr和shared_ptr一樣都是共有繼承自_Ptr_base。
2.它和shared_ptr的構造大體上相同,不過在增加引用計數的值時,shared_ptr是增加_Uses的值,而weak_ptr是增加_Weaks的值。同樣在類對象銷燬時,share_ptr通過_Uses減1是否爲0來判斷是否銷燬,而weak_ptr是通過_Weak減1是否爲0來判斷。

這是兩個節點的連接過程的示意圖:

下面我們來一步步看連接過程:(ref爲引用計數)
1.sp1的_Pnext中的ptr指向第二個節點,sp1的引用計數ref和sp2的引用計數共用,此時保持sp2引用計數中的Uses1不變,使Weaks增加爲2;
2.同樣的,使sp2中的_Pre的ptr指向第一個節點,然後讓它的ref共用第sp1的引用計數,使sp1引用計數中的Uses不變,Weaks變爲2;
出了函數的作用域,來銷燬節點:
1.讓sp2引用計數中的Uses從1減爲0,調用sp2的析構函數,將sp2節點銷燬。
2.sp2被銷燬,所以它其中的_Pre也被銷燬,因爲sp2中_Pre被sp1所引用,所以爲sp1的引用計數中的Weaks從2減爲1;
3.sp2引用計數中Weaks從2減爲1不爲0,所以sp2的引用計數空間不會被銷燬;
4.銷燬sp1,sp1的Uses從1減爲0,銷燬sp1這個節點,調用sp1的析構函數;
5.sp1被銷燬,sp1中的_PNext也被銷燬,因爲sp1的_PNext被sp2所引用,所以sp1中的Weak減1爲0,銷燬sp2的引用計數空間;
6,sp1的Weks從1減爲0,銷燬sp1的引用計數空間;
就這樣,四塊空間被銷燬,避免了循環引用帶來的內存泄漏問題。

發佈了46 篇原創文章 · 獲贊 16 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章