C++ 智能指针

本文介绍了C++中的智能指针,包括RAll思想、智能指针的作用以及如何解决浅拷贝问题。详细讲解了auto_ptr、unique_ptr、shared_ptr的实现原理和应用场景,其中unique_ptr通过禁止拷贝和赋值防止浅拷贝,shared_ptr则通过引用计数实现资源共享。文章还提到了shared_ptr的线程安全问题和循环引用问题,并介绍了weak_ptr如何解决循环引用。

智能指针

智能指针的使用以及原理
智能指针的思想:RAll
RAll是一种利用对象声明周期来控制程序资源的一种技术。它可以在对象构造时获取资源,在对象析构时候释放资源,完全不用用户自己释放,当然也解决了用原生态指针的缺陷。
RAll思想的好处
1.不用自己显示释放申请的资源,用户不用考虑什么术后该释放资源,将这些交给编译器即可。
2.用这种方法,对象所需的资源在其生命周期内始终保持有效。

下面我们用RAll思想来设计一个智能指针:

template<class T>
class Sptr
{
public:
	Sptr(T* ptr=nullptr)
		:_ptr(ptr)
	{}
	~Sptr()
	{
		if (_ptr)
		{
			delete _ptr;
			_ptr = nullptr;
		}
	}
	T* operator->()const//智能指针也是指针,也要有指针的基本功能
	{
		return _ptr;
	}
	T& operator*()const 
	{
		return *_ptr;
	}
private:
	T* _ptr;
};

上述是一个基本的智能指针。我们可以发现,因为涉及资源管理,所以当用一个指针去拷贝另一个指针时,就会产生浅拷贝,并且不能用深拷贝来解决。

因此综上所述我们可以得出在所有不同的智能指针中
1.因为RAll思想,资源可以自动释放
2.定义的智能指针类中,必须有普通指针类似的行为:operator*()以及operator->()
3.解决浅拷贝

auto_ptr
为了解决浅拷贝的问题,在C++98中,提供了auto_ptr智能指针。
注:所有C++库中智能指针都定义在memory这个头文件中。

通过使用auto_ptr我们可以发现,其解决浅拷贝方式是:资源转移,也就是所拷贝结束后,前面的指针置为空。只有后面的指针有地址。

下面我们来实现这个智能指针:

//模拟智能指针
namespace My
{
	template<class T>
	class auto_ptr
	{
	public:
		auto_ptr(T* ptr = nullptr)
			:_ptr(ptr)
		{}
		~auto_ptr()
		{
			if (_ptr)
			{
				delete _ptr;
				_ptr = nullptr;
			}
		}

		T* operator->()const//智能指针也是指针,也要有指针的基本功能
		{
			return _ptr;
		}
		T& operator*()const
		{
			return *_ptr;
		}
		auto_ptr(auto_ptr<T>& p)//此处参数前面不能用const,因为要将前面的指针值为空
			: _ptr(p._ptr)
		{
			p._ptr = nullptr;
		}
		auto_ptr<T>& operator=(auto_ptr<T>& p)
		{
			if (this != &p)
			{
				if (_ptr)
					delete _ptr;
				_ptr = p._ptr;//赋值
				p._ptr = nullptr;//断开前面指针
			}
			return *this;
		}
	private:
		T* _ptr;
	};
}

上述就是auto_ptr的模拟实现。但是我们发现因为把前面的指针已经与资源断开练联系,所以无法操作自己的资源。这也就是资源转移的缺陷,所以这种类型的auto_ptr在很多情况下不能用。

在解决浅拷贝时,除了上述资源转移的方法,还有一种方法也可以解决浅拷贝——加一个判定条件,让资源只释放一次。
我们来实现一下这个代码:

//模拟第二种解决办法
namespace My
{
	template<class T>
	class auto_ptr
	{
	public:
		auto_ptr(T* ptr = nullptr)
			:_ptr(ptr)
			, adjust(false)
		{
			if (_ptr)
				adjust = true;
		}
		~auto_ptr()
		{
			if (_ptr&&adjust)
			{
				delete _ptr;
				_ptr = nullptr;
			}
		}

		T* operator->()const//智能指针也是指针,也要有指针的基本功能
		{
			return _ptr;
		}
		T& operator*()const
		{
			return *_ptr;
		}
		auto_ptr(const auto_ptr<T>& p)//此处不用改变参数中的资源,但函数中需要访问判定条件,则给adjust前加mutable关键字即可
			: _ptr(p._ptr)
			, adjust(p.adjust)
		{
			p.adjust = false;
		}
		auto_ptr<T>& operator=(const auto_ptr<T>& p)
		{
			if (this != &p)
			{
				if (_ptr&&adjust)
					delete _ptr;

				_ptr = p._ptr;//赋值
				adjust = p.adjust;
				p.adjust = false;
			}
			return *this;
		}
	private:
		T* _ptr;
		mutable bool adjust;
	};
}

上述为第二种解决浅拷贝的办法:加个判定条件,让资源只释放一次。但这种方法也有缺陷:有可能造成野指针,从而使代码崩溃。

unique_ptr
虽然auto_ptr解决了浅拷贝问题,但还是存在很多缺陷。所以在C++11中提供了更靠谱的智能指针unique_ptr。

unique_ptr解决浅拷贝的方式资源独占(只能一个对象使用,不能共享)也就是禁止调用拷贝构造和赋值运算符重载,使其不能拷贝以及赋值。(防拷贝)

下面来重点模拟实现一下unique_ptr:

namespace My
{
	template<class T>
	class unique_ptr
	{
	public:
		unique_ptr(T* ptr = nullptr)
			:_ptr(ptr)
		{}
		~unique_ptr()
		{
			if (_ptr)
			{
				delete _ptr;
				_ptr = nullptr;
			}
		}

		T* operator->()const//智能指针也是指针,也要有指针的基本功能
		{
			return _ptr;
		}
		T& operator*()const
		{
			return *_ptr;
		}
		/*
		C++98中
		将拷贝构造函数和赋值构造函数只声明,可以让外部无法实现拷贝以及赋值,
		但是用户有可能自己在类外定义,所以拷贝构造函数和赋值构造函数必须给定为private权限,这样用户
		就不会在类外调用拷贝构造函数
	private:
		unique_ptr(const unique_ptr<T>& up);
		unique_ptr<T>& operator=(const unique_ptr<T>&);*/

		// C++11 禁止调用拷贝构造和赋值运算符重载,可以在函数后 =delete
		unique_ptr(const unique_ptr<T>&) = delete;  //默认成员函数 = delete : 告诉编译器,删除该默认成员函数
		unique_ptr<T>& operator=(const unique_ptr<T>&) = delete;

	private:
		T* _ptr;
	};
}


上述为unique_ptr解决浅拷贝的方法,这种方法虽然有些直接,但解决了浅拷贝以及之前的缺陷。

另外在释放资源期间,由于我们开辟的空间的方式不同,所以如果将释放资源的方式,固定成delete释放,就只能管理new出来的空间,不能任意处理其他类型的资源了。所以需要用仿函数让释放资源的方式丰富起来,使其指针更智能。也就是定制一个删除器。

以下是代码模拟:

template<class T>
class DFDef//用仿函数,定制出只对new出来的资源进行释放
{
public:
	void operator()(T*& ptr)
	{
		if (ptr)
		{
			delete ptr;
			ptr = nullptr;
		}
	}
};
template<class T>//只对malloc、calloc、realloc出来的资源进行释放
class FREE
{
public:
	void operator()(T*& ptr)
	{
		if (ptr)
		{
			free(ptr);
			ptr = nullptr;
		}
	}
};
class FCLOSE//只对文件开始的资源进行释放
{
public:
	void operator()(FILE*& ptr)
	{
		if (ptr)
		{
			fclose(ptr);
			ptr = nullptr;
		}
	}
};
namespace My
{
	template<class T, class df = DFDef<T>>//默认为对new申请的资源进行释放
	class unique_ptr
	{
	public:
		unique_ptr(T* ptr = nullptr)
			:_ptr(ptr)
		{}
		~unique_ptr()
		{
			if (_ptr)
			{
				df d;
				d(_ptr);
			}
		}
		T* operator->()const//智能指针也是指针,也要有指针的基本功能
		{
			return _ptr;
		}
		T& operator*()const
		{
			return *_ptr;
		}
		unique_ptr(const unique_ptr<T>&) = delete; 
		unique_ptr<T>& operator=(const unique_ptr<T>&) = delete;
	private:
		T* _ptr;
	};
}
void test()
{
	My::unique_ptr<int> p(new int);
	My::unique_ptr<int,FREE<int>> p2((int*)malloc(sizeof(int)));//根据不同类型,来对应调用不同的释放方式。
	My::unique_ptr<FILE, FCLOSE>p3(fopen("1.txt", "wb"));
}

shared_ptr
虽然unique_ptr解决了浅拷贝的问题,且基本没有问题。但是应用场景受限若遇到对于多个资源之间需要共享资源的场景,unique_ptr无法实现

所以为了解决unique_ptr的问题,在C++11中提出了shared_ptr。此指针不仅解决了浅拷贝的问题,同时可以多个对象之间共享资源。
shared_ptr解决浅拷贝的方式采用引用计数的方式,就是记录一块资源被多少对象在使用,如果计数为1那么说明只剩最后一个对象在访问资源,则可以释放资源。不为1则不释放。

下面我们来模拟一下shared_ptr:

namespace My
{
	template<class T>
	class shared_ptr
	{
	public:
		shared_ptr(T* ptr = nullptr)
			:_ptr(ptr)//const类型的变量、引用类型的变量、以及包含类类型的对象 这三个变量一般
			//需要在初始化列表的位置进行初始化
			, _pcount(nullptr)
		{
			if (ptr)//若用户给的资源不为空,则记一个数。
				_pcount = new int(1);
		}
		~shared_ptr()
		{
			if (_ptr && (--*_pcount == 0))
			{
				delete _ptr;
				delete _pcount;
			}
		}
		T* operator->()
		{
			return _ptr;
		}
		T& operator*()
		{
			return *_ptr;
		}
		shared_ptr(const shared_ptr<T>& sp)
			:_ptr(sp._ptr)
			, _pcount(sp._pcount)
		{
			if (_ptr)
				++*_pcount;
		}
		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			if (this != &sp)
			{
				if (_ptr&&--*_pcount == 0)//被赋值的对象与旧资源断开,
					//如果被赋值的对象是最后一个使用该块资源的对象,则释放旧的资源与计数。
					//否则,不用释放旧资源,因为还有对象在用此资源。
				{
					delete _ptr;
					delete _pcount;
				}
				_ptr = sp._ptr;//与赋值的对象共享资源和计数
				_pcount = sp._pcount;
				if (_ptr)//如果新资源不为空,计数+1;
					++*_pcount;
			}
			return *this;
		}

		int show__pcount()
		{
			return *_pcount;
		}
	private:
		T* _ptr;
		int* _pcount;
	};
}

以上代码就是shared_ptr的模拟实现。除此外我们还需考虑一点,就是在释放空间时,如果固定用delete来释放资源,则只能管理new出来的资源,对于其他类型资源,无法进行释放操作。这就是代码太局限了。所以我们还需要 定制删除器:使用户可以控制资源具体的释放操作。我们常用仿函数来定制不同的释放方式,然后通过模板参数传入即可。 定制删除器的方法与unique_ptr中相同

注意shared_ptr中计数不是线程安全的。因为当有多线程时,很可能会引起计数同时被改写,而造成计数数据不安全。所以就需要进行——加解锁操作,以保证当一个线程对计数进行操作时,其他线程不能对计数进行操作,当此时的操作完时,其他线程才能继续操作。虽然加锁保证计数安全,但不保证用户数据安全。
举例说明:

#include <mutex>//要用加解锁,必须要包含头文件
void Add ()
{
	mutex* _pMutex=new mutex;
	_pMutex->lock();//加锁
	
	...//进行数据操作
	
	_pMutex->unlock();//解锁
}

shared_ptr最大的缺陷:可能会造成循环引用,造成资源泄露。
下面通过一个双向链表的例子来看看什么是循环引用

struct SlistNode
{
	SlistNode(int data = 0)
		:pre(nullptr)
		, next(nullptr)
		, _data(data)
	{
		cout << "ListNode(int):" << this << endl;
	}
	~SlistNode()
	{
		cout << "~ListNode():" << this << endl;
	}
	shared_ptr<SlistNode> pre;//前一个指针域
	shared_ptr<SlistNode> next;//后一个指针域
	int _data;
};
void test()
{
	shared_ptr<SlistNode> sp1(new SlistNode(10));
	shared_ptr<SlistNode> sp2(new SlistNode(20));
	cout << sp1.use_count() << endl;
	cout << sp2.use_count() << endl;
	sp1->next = sp2;//讲个两个节点连起来
	sp2->pre = sp1;
	cout << sp1.use_count() << endl;
	cout << sp2.use_count() << endl;
}

在这里插入图片描述
那么怎么解决shared_ptr的循环引用问题呢,就用weak_ptr
weak_ptr
1.weak_ptr的实现原理和shared_ptr类似——也是用计数的方式。
2.weak_ptr的对象不能独立的管理资源,必须配合shared_ptr来使用。一起解决循环引用的问题

代码如下:

struct ListNode
{
	ListNode(int data = 0)
// 	: pre(nullptr)因为weak_ptr不能独立的管理资源,所以pre、next不需要初始化。
// 	, next(nullptr)
	: _data(data)
	{
		cout << "ListNode(int):" << this << endl;
	}
	~ListNode()
		cout << "~ListNode():" << this << endl;
	//shared_ptr<ListNode> pre;
	//shared_ptr<ListNode> next;
	weak_ptr<ListNode> pre;
	weak_ptr<ListNode> next;
	int _data;
};
//补充:
// weak_ptr<int> sp1;可以编译成功
//weak_ptr<int> sp2(new int);编译失败--原因:weak_ptr不能独立管理资源。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值