【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;
}

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