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

本文深入浅出地介绍了智能指针的概念、发展历程及其实现方式,包括管理权转移、简单粗暴法(防拷贝)和引用计数版本,并探讨了它们各自的优缺点。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

智能指针

____________________________________________________



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

因为它说白了就是个管理资源的。智能指针的原理就是管理资源的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_ptr 只对shared_ptr 进行引用不改变其引用计数,当被观察的shared_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 关键字,因为我们有智能指针。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值