C++——智能指针

时间:2024-11-12 15:09:21

        在C++中我们自己无论是用new 还是malloc或者是其他的方式申请了资源都要使用对应的方法去释放资源,如果不释放资源,就会造成资源泄漏的问题。因为异常处理机制的存在,如果我new了一个资源以后,我们就需要delete,但是在new以后到delete这段代码直接可能会抛出异常,这就会导致动态申请的内存空间没有被释放,所以我们可以在捕捉到异常以后先delete然后再抛出这个异常;但是这里就有另外一个问题,new本身可能也会抛出异常的,也就是说资源还没有申请就已经抛出异常了,此时捕捉到异常后去释放空间就会发生错误了。因为处理这些情况特别复杂,所以就有了RAII的思路,也就是Resource Acquisition Is Initialization,就是在申请一块资源时,我们不自己管理这个资源,直接把这个资源交给一个类来管理,那么在这个对象销毁的时候,这块资源也就会被自动回收了。智能指针就是按照这种思路设计的,智能指针模拟的是普通指针的行为,但是它是一个对象,在这个对象所在的函数栈销毁之前,这个对象就会被销毁,就可以实现资源在对象的生命周期内有效,出了这个生命周期,资源自动回收。所以智能指针会重载operator*/operator->/operator[]等操作符,方便访问资源。

一、智能指针的使用

        在C++中我们要使用智能指针就需要包含<memory>的头文件,智能指针有好几种,处理weak_ptr,其他的智能指针都符合RAII的特点。

        1、auto_ptr

        auto_ptr是在C++98就被设计出来的智能指针,它的设计非常糟糕,在以后尽量不要使用。它的特点是在拷贝时会被拷贝对象对资源的管理权限转移给拷贝对象,这就意味着,在使用auto_ptr时,如果使用了拷贝,那么被拷贝对象就不再管理资源了,也就悬空了,如果这个时候再去使用这个被拷贝对象,就会出现访问错误的问题。

        2、unique_ptr

        unique_ptr的特点是它不支持拷贝,但支持移动,在不需要拷贝的场景下使用它很方便。这里的unique_ptr虽然支持移动赋值,但是在使用unique_ptr时,它的被赋值对象一定是一个右值,也就是说,如果我们用unique_ptr去赋值另外一个unique_ptr也会转移资源的管理权限,但是被赋值的unique_ptr一定是被move过的,所以我们是知道被赋值的unique_ptr的资源是会被转移的,就能避免出现访问错误的问题。

        3、share_ptr

        share_ptr的特点是支持拷贝也支持移动,它的底层是用了引用计数的方式实现的,后面会详细讲解。

        4、weak_prt

        weak_prt和上面的智能指针都不一样,它不支持RAII,本质上weak_prt是为了解决share_ptr中循环引用的问题,在后面也会详细介绍。

        智能指针析构时默认是进⾏delete释放资源,这也就意味着如果不是new出来的资源,交给智能指 针管理,析构时就会崩溃。智能指针⽀持在构造时给⼀个删除器,所谓删除器本质就是⼀个可调⽤ 对象,这个可调⽤对象中实现你想要的释放资源的⽅式,当构造智能指针时,给了定制的删除器, 在智能指针析构时就会调⽤删除器去释放资源。因为new[]经常使⽤,所以为了简洁⼀点, unique_ptr和shared_ptr都特化了⼀份[]的版本,就可以管理new []的资源。shared_ptr 和 unique_ptr的构造函数都得使⽤explicit修饰,防⽌普通指针隐式类型转换成智能指针对象。

二、智能指针的原理

        1、auto_part

        auto_part的原理就是在完成拷贝的时候就要自己对于资源的管理权转移出去,有一点就是需要特殊判断自己给自己赋值的情况,以及赋值重载时,要先释放自己管理的资源。

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

		auto_ptr(auto_ptr<T>& ap)
			:_ptr(ap._ptr)
		{
			//转移管理权
			ap._ptr = nullptr;
		}

		auto_ptr<T>& operator=(auto_ptr<T>& ap)
		{
			//判断自己给自己赋值的情况
			if (&ap != this)
			{
				//释放当前资源
				if (_ptr)
					delete _ptr;

				//转移管理器
				_ptr = ap._ptr;
				ap._ptr = nullptr;
			}

			return *this;
		}

		~auto_ptr()
		{
			if (_ptr)
				delete _ptr;
		}

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

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

	privet:
		T* _ptr;
	};

        2、unique_ptr

        unique_ptr不能拷贝只能移动,所以我们把拷贝声明出来加上delete即可。

	template<class T>
	class unique_ptr
	{
		//防⽌普通指针隐式类型转换成智能指针对象
		explicit unique_ptr(T* ptr)
			:_ptr(ptr)
		{}

		unique_ptr(const unique_ptr<T>& up) = delete;
		unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;

		unique_ptr(unique_ptr<T>&& up)
		{
			std::swap(_ptr, up._ptr);
		}

		unique_ptr<T>& operator=(unique_ptr<T>&& up)
		{
			if (this != up._ptr)
			{
				if (_ptr)
					delete _ptr;

				std::swap(_ptr, up._ptr);
			}

			return *this;
		}

		~unique_ptr()
		{
			if (_ptr)
				delete _ptr;
		}

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

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

	private:
		T* _ptr;
	};

        3、share_ptr

        share_ptr中最重要的就是要解决多个对象同时管理一块资源要怎么析构的问题,这里可能会觉得添加一个static的成员变量count,当初始化一个对象以后就让count++,析构时如果count不为1就直接--,如果count为1再释放资源。但是这种解决方式的思路是正确的,但是行为是错误的,如果用一个static的成员变量的话,static的成员变量是属于这个类的,并不是属于对象的,也就是说如果我现在申请了两个管理不同资源的share_ptr,那么此时的count却变成了,在析构第一个对象时就发生了资源泄漏的问题。

        所以这里解决问题的办法是引用计数,要单独使用一个指针去指向一块空间,这块空间的作用就是用来存储当前管理这块资源的对象的个数,当有一个对象管理这块空间时,这个引用计数就要++,当一个对象被销毁时,这个引用计数就要--,当这个引用计数--到0时,就表示这块资源没有被对象管理了,此时就可以释放这块资源了。

        

	template<class T>
	class share_ptr
	{
		//防⽌普通指针隐式类型转换成智能指针对象
		explicit share_ptr(T* ptr)
			:_ptr(ptr)
			, _pcount(new int(1))
		{}

		//定制删除器版本
		template<class D>
		share_ptr(T* ptr,D del)
			:_ptr(ptr)
			, _pcount(new int(1))
			,_del(del)	
		{}

		shared_ptr(const share_ptr<T>& sp)
			:_ptr(sp._ptr)
			,_pcount(sp._pcount)
			,_del(sp.del)
		{
			++(*_pcount);
		}

		shared_ptr<T>& operator=(const share_ptr<T>& sp)
		{
			if (this != sp._ptr)
			{
				release();

				_ptr = sp._ptr;
				_pcount = sp._pcount;
				_del = sp._del;
				++(*_pcount);
			}

			return *this;
		}

		~share_ptr()
		{
			release();
		}

		void release()
		{
			if (--(*_pcount) == 0)
			{
				_del(_ptr);
				delete _pcount;
				_ptr = nullptr;
				_pcount = nullptr;
			}
		}

		int ues_count() const
		{
			return *_pcount;
		}

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

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

	private:
		T* _ptr;
		int* _pcount;
		//不传入删除器默认调用delete
		function<void(T*)> _del = [](T* ptr) {delete ptr; };
	};

        以上实现的智能指针都是简单版本的,库里的实现和功能都要复杂很多,这里只实现了最简单的逻辑

三、share_ptr和weak_ptr

        1、shared_ptr的循环引用问题

        shared_ptr在一般的场景下都能很好的适配,但是在循环引用的场景下,就会造成资源泄漏。什么是循环引用呢?可以想象成类似与循环链表的状态,在n1中管理着一个资源同时n1中的_next指向n2,n2管理者另外一个资源,同时n2中的_prev指向n1。此时n1和n2的引用计数都是2。这个时候我们来分析一下什么时候会释放资源就会发现问题了,n1的资源现在有两个地方管理着,分别是n1和n2的_prev,同理n2的资源也有两个地方管理着,分别是n2和n1中的_next。右边的结点什么时候释放呢,左边的_next析构以后右边结点就释放了;左边是_next什么时候析构呢,_next是左边的成员,左边结点释放的时候_next就析构了;左边结点什么时候释放呢,右边的_prev析构的时候左边结点就释放了;_prev是右边的成员,右边结点释放的时候_prev就析构了。这样在销毁n1和n2对象时,出了当前栈帧,n1资源和n2资源的计数都还是1,资源都没有被回收,就发生了资源泄漏。

        此时我们只要把结构里_prev和_next的类型换成weak_ptr就能解决问题了,把weak_ptr绑定到shared_ptr上时不会增加它的引用计数,_next和_prev不参与资源释放管理逻辑,就成功打破了循环引⽤。

        

        2、weak_ptr

        weak_ptr不支持RAII,也不支持访问资源,weak_ptr在构造时不支持绑定到资源上,只支持绑定到shared_ptr,在绑定到shared_ptr时,它不增加shared_ptr的技术。weak_ptr没有重载operator*和operator->等,因为它不参与资源的管理。weak_ptr支持使用expired()函数检查指向的资源是否过期了,也支持使用use_count获取shared_ptr的引用计数。当weak_ptr想要访问资源时,可以调用lock返回一个管理资源的shared_ptr如果资源以及被释放,那么返回的shared_ptr是一个空对象,如果资源没有被释放,那么通过这个返回的shared_ptr来访问资源就是安全的。