智能指针与弱指针解决循环引用

       当我们学会使用智能指针的时候,会发现他有很多好处,但库里面提供的智能指针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的引用计数空间;
就这样,四块空间被销毁,避免了循环引用带来的内存泄漏问题。

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值