C++程序员修炼手册--智能指针

一,智能指针的使用和原理

1.1,RAII

        RAII是基于对象的生命周期来控制程序资源(如内存,网络连接,互斥量)等,在对象构造时获取对象的资源,在对象析构的时候释放资源。实际上就是把管理一个资源责任交给一个对象,这样做的好处是:不需要显式的释放资源,对象在其生命周期内一直有效。

使用这种思想,我们可以模拟实现一个SmartPtr类实现对资源的管理。

#include<iostream>
using namespace std;
struct Date
{
	int _year;
	int _month;
	int _day;
};	
namespace Etta
{
	template<class T>
	class SmartPtr
	//这种指针无法对对象进行拷贝和赋值。
	//如果拷贝构造和赋值,就会重新开空间,实现深拷贝,且对相同资源做了多次管理,
	//不可靠且浪费资源,由此引出了auto_ptr 
	{
		public:
			SmartPtr(T*ptr=nullptr)
			:_ptr(ptr)
			{}
			~SmartPtr()
			{
				if(_ptr)
				{
					delete _ptr;
				}
			}
		private:
			T*_ptr;
	};	
	void test()
	{
			//把对象成员交给类取管理,当出了作用域以后,就对类析构,释放对象资源 
			SmartPtr<int> sp1(new int);
			*sp1 = 10;
			cout<<*sp1<<endl;
			SmartPtr<Date> sparray(new Date);
			sparray->_year = 2018;
			sparray->_month = 1;
			sparray->_day = 1;
	}
} 

1.2,智能指针

上述SmartPtr不能成为智能指针,因为不具有解引用,->功能,所以我们需要重载->和*才能算是一个智能指针。

#include"SmartPtr.h"
// auto_ptr的问题:当对象拷贝或者赋值后,前面的对象就悬空了
// C++98中设计的auto_ptr问题是非常明显的,所以实际中很多公司明确规定了不能使用auto_ptr
namespace Etta
{
	template<class T>
	class auto_ptr
	{
		public:
			auto_ptr(T*ptr=nullptr)
				:_ptr(ptr)
			{}
			~auto_ptr()
			{
				if(_ptr)
				{
					delete _ptr;
				}
			}
			//拷贝构造和赋值、
			// 一旦发生拷贝,就将ap中资源转移到当前对象中,然后另ap与其所管理资源断开联系,
			// 这样就解决了一块空间被多个对象使用而造成程序奔溃问题
			auto_ptr(auto_ptr<T>&ap)
			:_ptr(ap._ptr)
			{
				ap._ptr=nullptr; 
				
			} 
			auto_ptr<T>& operator=(const auto_ptr<T>&ap)
			{
				if(this!=&ap)
				{
					if(_ptr)
					{
						delete _ptr;
					}
					_ptr=ap._ptr;
					ap._ptr=nullptr;
				} 
				return *this;
			}
			T*operator->()
			{
				return _ptr;
			}
			
			T&operator*()
			{
				return *_ptr;
			}
		private:
			T* _ptr;
	};
	void test_auto_ptr()
	{
		auto_ptr<Date> ap(new Date);
		// 现在再从实现原理层来分析会发现,这里拷贝后把ap对象的指针赋空了,导致ap对象悬空
		// 通过ap对象访问资源时就会出现问题。
		auto_ptr<Date> copy(ap);
		ap->_year = 2018;
	}
	
}

但是auto_ptr的赋值和拷贝构造是通过对资源的管理权转移,导致原指针悬空,所以很多公司禁止了使用这种智能指针,所以衍生出unique_ptr。

智能指针的特性:

                RAII特性

                具有像指针一样的行为,支持->和*。

1.3,unique_ptr(不支持拷贝和赋值)

unique_ptr的实现原理:简单粗暴的防拷贝

//简单粗暴的防拷贝,简化模拟实现了一份UniquePtr来了解
namespace Etta
{
template<class T>
	class unique_ptr
	{
	public:
		// RAII
		unique_ptr(T* ptr)
			:_ptr(ptr)
		{}

		~unique_ptr()
		{
			cout << "delete:" << _ptr << endl;
			delete _ptr;
		}

		// 可以像指针一样使用
		T& operator*()
		{
			return *_ptr;
		}

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

		unique_ptr(const unique_ptr<T>&) = delete;
		unique_ptr<T>operator=(const unique_ptr<T>&) = delete;
	private:
		T* _ptr;
	};

	void test_unique_ptr()
	{
		//std::unique_ptr<int> sp1(new int);
		// 拷贝
		//std::unique_ptr<int> sp2(sp1);

		unique_ptr<int> sp1(new int);
		// 拷贝
		//bit::unique_ptr<int> sp2(sp1);
	}
}

缺点就是不支持拷贝和赋值,为了更加靠谱和支持拷贝构造,C++11提出了shared_ptr

1.4,shared_ptr

 1, shared_ptr的原理:

        是通过引用计数的方式来实现多个shared_ptr对象之间共享资源

1. shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。
2. 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一
3. 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源
4. 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了

5,因为存在计数,所以多个线程访问时,容易导致两个线程同时+,本来是3,但实际上变成2,所以为了防止这种情况,需要在拷贝和赋值的时候加锁处理。

模拟实现:

#include<thread>
#include<mutex>
#include<iostream>
using namespace std;
namespace Etta
{
	template<class T>
	class share_ptr
	{
		public:
			share_ptr(T* ptr=nullptr)
			:_ptr(ptr)
			,_count(new int(1))
			,_mtx(new mutex)
			{}
			~share_ptr()
			{
				ReleaseRef();	
			}
			share_ptr(const share_ptr<T>&sp)
			:_ptr(sp._ptr)
			,_mtx(sp._mtx)
			,_count(sp._count)
			{ 
				AddRef();
			}
			//将sp赋值给this
			//先减去_ptr的计数,再把sp的空间给给_ptr,再对_ptr加加 
			share_ptr<int>&operator=(const share_ptr<T>&sp)
			{
				if(_ptr!=sp._ptr)
				{
					ReleaseRef();
					_ptr=sp._ptr;
					_mtx=sp._mtx;
					_count=sp._count;
					AddRef();
				}
				return *this;
			} 
			
			T* get() const
			{
				return _ptr;
			} 
			T&operator*()
			{
				return *_ptr;
			} 
			T*operator->()
			{
				return _ptr;
			}
			int use_count()
			{
				return *_count;
			}
		private:
			T *_ptr;
			mutex* _mtx;
			int *(_count);
			void AddRef()
			{
				//为什么要加锁,因为不加锁,在两个线程同时拷贝一个对象的话,如果都进来++、
				//容易导致产生加了2次,但实际上值为2
				//产生线程错误 
				_mtx->lock();
				++(*_count);
				_mtx->unlock();
			}	
			void ReleaseRef()
			{
				_mtx->lock();
				bool flag=false;
				if(--(*_count)==0)
				{
					cout << "delete:" << _ptr << endl;
					delete _ptr;
					delete _count;
					flag=true;
				}
				_mtx->unlock();
				if(flag==true)
				{
					delete _mtx;
				}
			}
	};
}

2,循环引用问题

先看这个问题:我们使用shared_ptr对节点进行管理。

看看跑起来结果怎么样。

struct ListNode
{
		std::shared_ptr<ListNode> _next;
		std::shared_ptr<ListNode> _prev;
		int _val;
		
		~ListNode()
		{
			cout << "~ListNode()"<<endl;
		}
};	
void test_shared_ptr_cycle()
{
		std::share_ptr<ListNode>node1(new ListNode);
		std::share_ptr<ListNode>node2(new ListNode);
		cout << node1.use_count() <<endl;
		cout << node2.use_count() << endl;
					
					
		node1->_next=node2;
		node2->_prev=node1;
					
		cout << node1.use_count() <<endl;
		cout << node2.use_count() << endl;
}
			

可以看到程序会崩掉,在最后没有调用ListNode的析构,导致这两个节点未被释放,

此时,

解决方案:在引用计数的场景下,把节点中的_prev和_next改成weak_ptr就可以了
原理就是:node1->_next = node2;和node2->_prev = node1;时weak_ptr的_next和_prev不会增加node1和node2的引用计数

1.5,weak_ptr

不参与资源管理,不增加shared_ptr管理资源的引用计数,可以像指针一样使用。

// 不参与资源管理,不增加shared_ptr管理资源的引用计数,可以像指针一样使用
	template<class T>
	class weak_ptr
	{
	public:
		weak_ptr()
			:_ptr(nullptr)
		{}

		weak_ptr(const shared_ptr<T>& sp)
			:_ptr(sp.get())
		{}

		weak_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			_ptr = sp.get();
			return *this;
		}

		// 可以像指针一样使用
		T& operator*()
		{
			return *_ptr;
		}

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

二,总结

智能指针主要有以下思想:

RAII:  RAII是基于对象的生命周期来控制程序资源,构造的时候管理,析构的时候释放。

shared_ptr:对指针的拷贝和赋值通过一个int*(count)进行++计数,析构--,加锁控制线程安全。

weak_ptr;对象内成员不参与资源的管理,只是作为一个指针使用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

想找后端开发的小杜

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值