【C++】三种智能指针(auto_ptr,unique_ptr,shared_ptr)

c++中的智能指针主要是基于RAII思想的。

不懂RAII思想的同学可以看看这篇博文-->RAII思想---利用对象生命周期来控制程序资源

智能指针的两大要素

  1. RAII思想:把资源交给这个对象管理
  2. 像指针一样的行为(重载operator* 和operator->
template<class T>
class SmartPtr
{
public://交给对象去管理
	SmartPtr(T* ptr=nullptr)
		:_ptr(ptr)
	{}
	~SmartPtr()
	{
		if (_ptr)
			delete[] _ptr;
	}
	//像指针一样的行为
	T& operator*()//对象出了作用域还在,所以返回引用
	{
		return *_ptr;
	}
	T* operator->()
	{
		return _ptr;//返回原生指针
	}
private:
	T* _ptr;
};

std::auto_ptr(C++98)

C++98版本的库中提供了auto_ptr的智能指针。

想要实现一个智能指针就要实现这几个功能:RAII思想,像指针一样的行为。但是对象会有拷贝构造和赋值,auto_ptr的原理是进行了管理权的转移(管理权的转移是当把a的值给b之后,就把a置成空),这是一种带有缺陷的智能指针,会导致对象悬空。

实现的原理如下:

template<class T>
class auto_ptr
{
public:
	auto_ptr(T* ptr)
		:_ptr(ptr)
	{}
	~auto_ptr()
	{
		if (_ptr != nullptr)
		{
			cout << "delete:" << _ptr << endl;
			delete _ptr;
		}
	}
	//拷贝构造ap1(ap2)---ap2拷贝构造ap1,此时把ap2置空,管理权交给ap1
	//this指针是ap1,ap是ap2
	auto_ptr(auto_ptr<T>& ap)
		:_ptr(ap._ptr)
	{
		ap._ptr = nullptr;//不执行此句会崩溃,同一资源释放了两次
	}
	//赋值 ap1=ap2
	auto_ptr<T>& operator=(auto_ptr<T>& ap)
	{
		if (this != &ap)//检测是否给自己赋值
		{
			//释放当前对象ap的资源
			if (_ptr)
				delete _ptr;
			//转移ap中的资源到当前对象去
			_ptr = ap._ptr;
			ap._ptr = NULL;
		}
		return *this;//支持连续赋值
	}
	T& operator*()
	{
		return *_ptr;
	}
	T* operator->()
	{
		return _ptr;
	}
private:
	T* _ptr;
};

auto_ptr是一种管理权转移的思想,由于转移之后原来的对象会被置空,会导致对象的悬空,如下:

void test_auto_ptr()
{
	cpp::auto_ptr<int> ap1(new int);
	cpp::auto_ptr<int> ap2(ap1);
	// *ap1 = 10;//拷贝构造后管理权转移,ap1悬空
	*ap2 = 20;

	cpp::auto_ptr<int> ap3(new int);
	//ap3 = ap2;//赋值把p2置空了
	*ap3 = 40;
}

这里导致原来的对象悬空了,auto_ptr是带有缺陷的早期设计,因此很多公司都会禁用这个auto_ptr。

std::unique_ptr(C++11)

C++11中开始提供更靠谱的unique_ptr。

unique_ptr是简单粗暴的防止拷贝,这种比较简单,效率高,但是功能不全面,不支持拷贝和赋值操作。具体实现思想如下:

template<class T>
	class unique_ptr
	{
	public:
		unique_ptr(T* ptr)
			:_ptr(ptr)
		{}
		~unique_ptr()
		{
			delete _ptr;
		}
		//拷贝
		unique_ptr(unique_ptr<T>& ap) = delete;
			
		//赋值 
		unique_ptr<T>& operator=(unique_ptr<T>& ap) = delete;
		
		T& operator*(){return *_ptr;}

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

shared_ptr(C++11)

C++11中开始提供更靠谱的并且支持拷贝的shared_ptr。

shared_ptr它的原理是引用计数,它的功能更加全面,支持拷贝构造。但是设计复杂,会有循环引用的问题。

这里提到一个概念,引用计数,引用计数表示这块空间被几个对象引用,有一个对象析构了,引用计数就会减一,但是并不是每次析构都会释放空间,而是最后一个引用它的对象才去释放它。

我们这里的引用计数只需要一个就行了,所以很容易想到把它定义成静态的,但是其实这也不可以,因为一旦一个类中的对象管理不同的空间,就如下图,Sp1和Sp2管理一块空间,Sp3和Sp4和Sp5管理另一块空间,用同一个引用计数的话,我们期望3,4,5析构后,这块空间也被释放掉,但是由于引用计数只减为2,,没有变为1,他认为你不是最后一个引用它的,所以不会释放掉空间,就会有内存泄漏问题。

所以这里的引用计数就不能跟着对象走,而是应该跟着空间走的,有一个空间就动态开辟一个引用计数跟着它,每个智能指针都能拿到该引用计数如下图:

    template<class T>
	class shared_ptr
	{
	public:
		shared_ptr(T* ptr)
			:_ptr(ptr)
			,_pcount(new int(1))//初始化,动态开辟
		{}
		~shared_ptr()
		{
			if (--(*_pcount) == 0)//引用计数为0时,最后一个指向它的释放它
			{
				cout << "delete:" << _ptr << endl;
				delete _ptr;
				delete _pcount;
			}
		}
		//拷贝
		shared_ptr(const shared_ptr<T>& sp) 
			:_ptr(sp._ptr)
			, _pcount(sp._pcount)
		{
			++(*_pcount);//同一个引用计数
		}

		//赋值 
		//sp1 = sp3;
		shared_ptr<T>& operator=(shared_ptr<T>& sp)
		{
			//if (this != &sp)//防止自己给自己赋值
			//两个对象的ptr如果相同就不进入,否则--再++是无用功
			//要是相同的对象,ptr相同就不作操作
			//要是不同的对象,但是指向同一块空间,ptr相同也不作操作
			if (_ptr != sp._ptr)
			{
				//判断要赋值对象的引用计数是不是1
				//如果是1,释放指针和引用计数再指向新的空间进行赋值
				if (--(*_pcount) == 0)
				{
					delete _ptr;
					delete _pcount;
				}
				//两个指针指向同一块空间,再++引用计数
				_ptr = sp._ptr;
				_pcount = sp._pcount;
				++(*_pcount);
			}
			return *this;
		}
		T& operator*(){return *_ptr;}
		T* operator->(){return _ptr;}
		int use_count()	
		{ 
			return *_pcount;
		}//查看引用计数是多少
	private:
		T* _ptr; 
		int* _pcount;//引用计数
	};

看到这里,很多童鞋肯定像我一样以为shared_ptr就到此结束了,其实并没有,它在用起来没有大问题,但是存在着一个问题,就是多线程的情况下的线程安全问题,来看下面这段代码:

void test_multi_thread_copy(cpp::shared_ptr<int>& sp,size_t n)
{
	for (size_t i = 0; i < n; ++i)//拷贝n次ptr
	{
		cpp::shared_ptr<int> copy(sp);
	}
}
void test_shared_ptr_safe()
{
	cpp::shared_ptr<int> sp(new int);
	std::thread t1(test_multi_thread_copy, sp, 1000);
	std::thread t2(test_multi_thread_copy, sp, 1000);
	cout << sp.use_count() << endl;
	t1.join();
	t2.join();
}

t1和t2是我们创建的两个线程,这两个线程都去拷贝ptr1000次,此时多个线程同时对该空间进行++,--操作,那么导致结果错误,并且打印出来的引用计数也是不确定的,说明此时的引用计数已经出现了问题,导致内存没有释放,会出现内存泄漏的问题。因为这里的 ++操作和 - - 操作不是原子的操作 ,存在线程安全问题。因此多线程编程时,要给这里的引用计数进行加锁操作,基于以上我们进一步完善shared_ptr。

template<class T>
	class shared_ptr
	{
	public:
		shared_ptr(T* ptr)
			:_ptr(ptr)
			,_pcount(new int(1))//初始化,动态开辟
			, _pmtx(new std::mutex)
		{}
		~shared_ptr()
		{
			Release();
			//if (--(*_pcount) == 0)//引用计数为0时,最后一个指向它的释放它
			//{
			//	cout << "delete:" << _ptr << endl;
			//	delete _ptr;
			//	delete _pcount;
			//}
		}
		void AddRef()//增加引用计数
		{
			_pmtx->lock();
			++(*_pcount);//同一个引用计数
			_pmtx->unlock();
		}
		void Release()//释放引用计数
		{
			_pmtx->lock();
			int flag = 0;
			if (--(*_pcount) == 0)
			{
				cout << "delete: " << _ptr << endl;
				delete _ptr;
				delete _pcount;
				flag = 1;
			}
			_pmtx->unlock();
			if (flag == 1){delete _pmtx;}
		}
		//拷贝
		shared_ptr(const shared_ptr<T>& sp) 
			:_ptr(sp._ptr)
			, _pcount(sp._pcount)
			, _pmtx(sp._pmtx)
		{
			//++(*_pcount);//同一个引用计数
			AddRef();//加锁,封装成函数
		}

		//赋值 
		//sp1 = sp3;
		shared_ptr<T>& operator=(shared_ptr<T>& sp)
		{
			//if (this != &sp)//防止自己给自己赋值
			if (_ptr != sp._ptr)
			{
				//if (--(*_pcount) == 0)
				//{
				//	delete _ptr;
				//	delete _pcount;
				//}
				Release();
				//两个指针指向同一块空间,再++引用计数
				_ptr = sp._ptr;
				_pcount = sp._pcount;
				_pmtx = sp._pmtx;
				AddRef();
				/*++(*_pcount);*/
			}
			return *this;
		}
		
		T& operator*()  {return *_ptr;}
		T* operator->() {return _ptr;}

		int use_count()	{ return *_pcount;}//查看引用计数是多少
	private:
		T* _ptr; 
		int* _pcount;//引用计数
		std::mutex* _pmtx;//定义一个锁,大家都锁在该锁上,所以定义为指针
	};

这样加锁之后,智能指针的引用计数的++和- - 是安全的,也支持拷贝构造,所以shared_ptr本身是安全的,智能指针的线程安全体现在引用计数的++和- -是线程安全的,但是它指向的对象不一定是线程安全的,因为智能指针指向的对象本身并不受它的管控。

shared_ptr的循环引用以及weak_ptr()

struct ListNode
{
	std::shared_ptr<ListNode> _prev;
	std::shared_ptr<ListNode> _next;
	~ListNode()
	{
		cout << "~ListNode()" << endl;
	}
};

void test_shared_ptr_cycle_ref()
{
	std::shared_ptr<ListNode> cur(new ListNode);
	std::shared_ptr<ListNode> next(new ListNode);
	
	cur->_next = next;
	next->_prev = cur;
}

这一段代码中用shared_ptr管理了cur和next,同时也管理了cur和next里面的_prev和_next,让智能指针share_ptr指向cur和next时,两个智能指针的引用计数都是1,再让cur的_next指向了next,让next的_prev指向了cur,此时两个智能指针的引用计数都变成了2,出了test_shared_ptr_cycle_ref函数之后,cur和next应该去调析构函数,但是此时智能指针的引用计数不为1,所以两个智能指针的引用计数- -,那么此时就没有完成节点的释放,因为发生了循环引用。可以理解为相互牵制,谁也无法进行释放,画个图理解一下:

  • cur和next两个智能指针对象指向两个节点,引用计数变成1,我们不需要手动delete。
  • cur的_next指向next,next的_prev指向cur,引用计数变成2。
  • cur和next析构,引用计数减到1,但是_next还指向下一个节点。但是_prev还指向上一个节点。
  • 也就是说_next析构了,next就释放了。
  • 也就是说_prev析构了,cur就释放了。
  • 但是_next属于cur的成员,cur释放了,_next才会析构,而cur由_prev管理,_prev属于next成员,所以这就叫循环引用,谁也不会释放。

那么这种问题要怎么解决呢?解决方案就是使用weak_ptr

struct ListNode
{
	std::weak_ptr<ListNode> _prev;
	std::weak_ptr<ListNode> _next;
	~ListNode()
	{
		cout << "~ListNode()" << endl;
	}
};

因为在执行cur->_next = next;next->_prev = cur;时,weak_ptr的_next和_prev不会增加cur和next的引用计数,这样就保证了正常的释放。

但是weak_ptr只能用于循环引用场景下,其他时候不能用

shared_ptr定制删除器

在上面写的代码中,都是一次只申请了一块空间,然后让智能指针管理,智能指针释放的时候直接调用delete即可,但是不排除有这么一种情况,那就是如果一次申请多块空间呢?

struct A
{
	~A()
	{
		cout << "~A()" << endl;
	}
};
//定制删除器
void test_shared_ptr_deletor()
{
	std::shared_ptr<A> sp(new A[10]);
}
int main()
{
	test_shared_ptr_deletor();
	system("pause");
	return 0;
}

那此时就需要用delete[]来进行释放,因此可以利用仿函数来定制删除器。

struct A
{
	~A()
	{
		cout << "~A()" << endl;
	}
};
template<class T>
struct DeleteArray
{
	void operator()(T* ptr)
	{
		delete[] ptr;
	}
};
//定制删除器
void test_shared_ptr_deletor()
{
	DeleteArray<A> del;
	std::shared_ptr<A> sp(new A[10],del);
}
int main()
{
	test_shared_ptr_deletor();
	system("pause");
	return 0;
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章