C++ — 智能指针的简单实现以及循环引用问题

智能指针

____________________________________________________



今天我们来看一个高大上的东西,它叫智能指针。 哇这个名字听起来都智能的不得了,其实等你了解它 你一定会有一 点失望的。。。。

因为它说白了 就是个管理资源的。智能指针的原理就是管理资源的RALL机 制,我们先来简单了解一下

RALL机制: RALL机制便是通过利用对象的自动销毁,使得资源也 具有了生命周期,有了自动销毁(自动回收)的功能。 RAII全称为

Resource  Acquisition Is Initialization,它是在一些面向对象语言中的一种惯用 法。RAII 源于C++,在 Java,C#,D,Ada

,Vala和Rust中也有应用。 资源分配 即初始化,定义一个类来封装资源的分配和释放,在构造函数完 成资源的 分配和初始化,在析

函数完成 资源的清理,可以保证资源的正确初始化和释 放。RAII要求,资源的有效期与 持有资源的对象的 生命期严格绑定,即由

对象的构造函数完成资源的分配(获取),同时由析构函数完成资源的释放。在 这种 要求下,只要对象能正确地析构,就不会出现资

源泄露问题。 RALL在这里就是简单提一下而已,现在我们来看我们 今天的主角智能指针。

 

智能指针(smart pointer)是存储指向动态分配(堆)对象指针的类。它的诞生理由就是,为粗心和懒的 设计的, 但是这个设计一定

不是反人类 的,因为无论你有多厉害只要你是人你总会有犯错误的时候,所 以智能 指针可以很好地 帮助我们, 程序员每次  new  出来 的内

都要手动  delete 。程序 员忘记  delete ,流 程太复 杂, 最终导致没有  delete

异常导致程序过早退出,没有执行  delete  的情况并不罕见。其实智能 指针只是怕你 忘了 delete,而专门设置出来的 个对象。有没有

感觉它顿时不够 智能呢,但是你绝对不能否 认它的实用性和 重要性。 现在我们来看看智能指针的使用吧:


对于编译器来说,智能指针实际上是一个栈对象,并非指针类型,在栈对象生命期即将结束时,智能指针通 过析 构函 数释放有它管理的

堆内存。所有智 能指针都重载了“ operator-> ”操作符,直接返回对象的引用,用 以操作 对象。访 问智能指针原来的方法则使用“ .

操作符。 先抛开智能指针的几个 版本不说,我们先来讲一下它里面的 * 和 -> 是 何进行运算符重载的。 下面是我定义的一个类,他

是为了实现原生指针的 * 和 -> 功能:


struct AA
{
	int a = 10;
	int b = 20;
};
template<class T>
class A
{
public:

	A(T* ptr)
		:_ptr(ptr)
	{}

	T* operator->()
	{
		return _ptr;
	}

	T& operator*()
	{
		return *_ptr;
	}

	A(A<T>& ap)
	{}
	A<T>& operator=(A<T>& ap)
	{}
	~A()
	{delete _ptr;}
protected:
	T* _ptr;
};

int main()
{
	A<int>ap1(new int);
	*ap1 = 10;
	A<AA>ap2(new AA);
	cout << *ap1 << endl;
	cout << (ap2->a)<<"  "<<(ap2->b) << endl;
	return 0;
}


请忽略这个粗糙的A类和AA结构体,我们的目的只是实现原生函数的功能,那么我的功能实现了吗?

           

这里结果没有一点问题,那么我们现在的注意点就应该放在这里是如何实现的:









智能指针的三大版本的实现==>



好了前面那些磨人的小妖精终于清理完了,现在我们真真正正的进入主题,智能指针的发展史以及它的常见的三个版本。

                       1.管理权转移   2.简单粗暴的防拷贝  3.引用计数版本

注意这里我只是实现简单的思想,可能写的不是很好,望大家指出帮助我改正错误。

管理权转移==>


这个智能指针是1998应用到VS上的,现在我们来实现第一个,何为管理权转移呢?


现在我列出该思想的实现代码:

template<class T>
class AutoPtr
{
public:

	AutoPtr(T* ptr)
		:_ptr(ptr)
	{}

	T* operator->()
	{
		return _ptr;
	}

	T& operator*()
	{
		return *_ptr;
	}

	AutoPtr(AutoPtr<T>& ap)
	{
		this->_ptr = ap._ptr;
		ap._ptr = NULL;
	}
	AutoPtr<T>& operator=(AutoPtr<T>& ap)
	{
		if (this != &ap)
		{
			delete this->_ptr;
			this->_ptr = ap._ptr;
			ap._ptr = NULL;
		}
		return *this;
	}
	~AutoPtr()
	{
		cout << "智能指针爸爸已经释放过空间了" << endl;
		delete _ptr;
	}
protected:
	T* _ptr;
};

int main()
{
	AutoPtr<int>ap1(new int);
	*ap1 = 10;
	AutoPtr<int>ap2(ap1);
	AutoPtr<int>ap3(ap2);
	*ap3 = 20;
	ap2 = ap3;
	cout << *ap2 <<endl;

	return 0;
}

现在我们先看看它使用普通操作时的结果如何:




现在的结果真的太符合我们的预料了,我们要的就是这样的结果,当你还沉浸自己成功的喜悦的时候 ,这里虽然 成功 实现了自动释放空

间的功能还有指 针的功能,但是看看下面这种情况: 我们把main函数内修改成这个样子:

int main()
 {
AutoPtr<int>ap1(new int);
*ap1 = 10;
AutoPtr<int>ap2(ap1);
cout << *ap1 << endl;
return 0;
 }






然后结果。。调试到这一步程序崩溃了,罪魁祸首就是AutoPtr<int>ap2(ap1),这里原因就是ap2完全 的夺取了 ap1的 管理权。然后

导致ap1无家可归, 访问它的时候程序就会崩溃。如果在这里调用ap2 = ap1 程序一样会崩溃 原因还是ap1 被彻彻底底的夺走一切,

以这种编程思想及其不符合C++思想, 所以它的 设计思想就是有一定的缺 陷。 所以一般不推荐使用Autoptr智能指针。 使用了也绝

对不能使用"="和拷贝构造。 历史在发展,所以我们见到接 下来这种想法:  


简单粗暴法(防拷贝)==>


scoped智能指针 属于 boost 库,定义在 namespace boost 中,包含头文件#include<boost/smart_ptr.hpp>  便可以 使用。

scoped智能指    AutoPtr智能指针  一样,可以方便的管理单个堆内存对象,特别的是, scoped智能指针  享所有权,避免

 AutoPtr智能指针 恼人的几个问 题,它直接就告诉用户我不提供"="和拷贝 构造这两个功能,你别用, 用了我也让你编不过去。

来看它的实现:

template<class T>
class ScopedPtr
{
public:
	ScopedPtr()
	{}
	AutoPtr(T* ptr)
		:_ptr(ptr)
	{}

	T* operator->()
	{
		return _ptr;
	}

	T& operator*()
	{
		return *_ptr;
	}
	~AutoPtr()
	{
		cout << "智能指针爸爸已经释放过空间了" << endl;
		delete _ptr;
	}

protected:
	ScopedPtr(ScopedPtr<T>& s);
	ScopedPtr<T> operator=(ScopedPtr<T>& s);
protected:
	T* _ptr;
};

它的意思就是,我根本不会提供拷贝构造 和 "="的功能,他强任他强,我就是这样。他确实解决上 一个智能指针 的问 题,他直接让用

户不能使用这个 功能,这个思想确实有点反人类。。 由于 scoped智能指针 独享所有权,当我们真真需要 复制智能指针时,需求便满足

了了,如此我们再 引入一 个智能 指针,专门用于处理复制,参数传递的情况。 这便是如下的shared 智能指针。


引用计数版本==>


接下来我们看最后一种,也就是我们现在经常用到的shared智能指针,等到智能指针发展到这一步也就很 成熟了 它已经 几乎完美的解

决所有功能,因为 它使用了引用计数版本当指向该片资源的*_num变成0的时候,释放该资源.


template<class T>
class shared
{
public:
	shared(T* ptr)
		:_ptr(ptr)
		, _num(new int(1))
	{
	}
	shared(const shared<T>& ap)
		:_ptr(ap._ptr)
		, _num(ap._num)
	{
		++(*_num);
	}
	shared<T>& operator=(const shared<T>& ap)
	{
		if (_ptr != ap._ptr)
		{
			Release();
			_ptr = ap._ptr;
			_num = ap._num;
			++(*_num);
		}
		return *this;
	}
	T* operator->()
	{
		return _ptr;
	}
	
	T& operator*()
	{
		return *_ptr;
	}
	void Release()
	{
		if (0 == (--*_num))
		{
			cout << "智能指针爸爸帮你释放空间了" << endl;
			delete _ptr;
			delete _num;
			_ptr = NULL;
			_num = NULL;
		}
	}
	~shared()
	{
		Release();
	}
protected:
	T* _ptr;
	int* _num;
};

int main()
{
	shared<int>ap1(new int);
	*ap1 = 2;
	shared<int>ap2(ap1);
	cout << *ap2 << endl;
	shared<int>ap3(new int);
	ap3 = ap1;

}

上面就是我实现的简易的shared智能指针,现在我们调用这个智能指针,我们来看看结果:





我们发现它完美的解决了一切功能,这个指针真的算是很完美的思想,不过你再完美也会有瑕疵,要不然也 不会 boost::weak_ptr

的存在, boost::weak_ptr的存在 就是为 boost::shared_ptr解决一点点瑕疵的。这个 问题藏 得极深 不会遇到的,但是当你真的

到的时候,我相信你会 绞尽 脑汁的找BUG,还是很难找的。 话不多说,现在我们来看下面这个例子:

struct ListNode
{
	int _data;
	shared_ptr<ListNode> _prev;
	shared_ptr<ListNode> _next;

	ListNode(int x)
		:_data(x)
		, _prev(NULL)
		,_next(NULL)
	{}
	~ListNode()
	{
		cout << "~ListNode" << endl;
	}
};
int main()
{
	shared_ptr<ListNode> cur(new ListNode(1));
	shared_ptr<ListNode> next(new ListNode(2));
	cur->_next = next;
	next->_prev = cur;
	cout << "cur" << "     " << cur.use_count() << endl;
	cout << "next" << "     " << next.use_count() << endl;
	return 0;
}

现在我们验证shared智能指针的缺陷,就不用我实现的那个了,那个好多功能我都没实现,我们用专家
shared_ptr智 能指针,

构造两个双向链表里 面的结点,这里这个双向链表可能有一点简陋,但是我们只 是需要 它的prev和next指针就够了。 现在我们运

行代码看看会发生什么情况:



现在cur和next指针所管理的结点现在都有两个指针指针管理,然后在这里会发生这样一件事:



循环引用一般都会发生在这种"你中有我,我中有你"的情况里面,这里导致的问题就是内存泄漏,这段空间 一直都没有释 放,现在很

明显引用计数在这 里就不是很合适了,但是shared_ptr除了这里不够完善,其他的 地方都是非常有用的东西, 所以编写者在这里补

充一个week_ptr,接下来我们看最后一 个智能指针week_ptr。


week_ptr==>


weak_ptr是为了配合shared_ptr而引入的一种智能指针,它更像是shared_ptr的一个助手而不是智能指针, 为它不具 有普通指针

的行为,没有重载 operator*和->,它的最大作用在于协助shared_ptr工作,像旁观者 那样 观测资源的使用情 况. 通俗一点讲就是,

weak_ptr  是专门为 shared_ptr  准备的。现在我们并不能根据 内部的 引用计数。 weak_ptr   boost::shared_ptr  的观察

对象,观察 者意味着 weak_p tr  只对 shared_ptr 进 行引用 不改变其引用计数,当被观 察的 shar ed_ptr  失效后,相应的

weak_ptr 也相应失效,然后它就什么都 不管光是个删 , 也就是这里的cur和next在析 构的时候 , 不用引用计数减一 , 直接删

结点就好。 这样也就间接地解决了循环引用的问题,当然week_ptr指针的功 能不是只有这一个。但是现在 我们只要 知道它可以解

决循环引用就好。

现在总结一下:

1、在可以使用 boost 库的场合下,拒绝使用 std::auto_ptr,因为其不仅不符合 C++ 编程思想。

2、在确定对象无需共享的情况下,使用 boost::scoped_ptr。

3、在对象需要共享的情况下,使用 boost::shared_ptr。

4、在需要访问 boost::shared_ptr 对象,而又不想改变其引用计数的情况下(循环引用)使用boost::weak_ptr。

5、最后一点,在你的代码中,尽量不要出现 delete 关键字,因为我们有智能指针。


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值