程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> C++入門知識 >> 智能指針 auto_ptr、scoped_ptr、shared_ptr、weak_ptr,auto_ptrscoped_ptr

智能指針 auto_ptr、scoped_ptr、shared_ptr、weak_ptr,auto_ptrscoped_ptr

編輯:C++入門知識

智能指針 auto_ptr、scoped_ptr、shared_ptr、weak_ptr,auto_ptrscoped_ptr


 什麼是RAII?

RAII是Resource Acquisition Is Initialization的簡稱,是C++語言的一種管理資源、避免洩漏的慣用法。

RAII又叫做資源分配即初始化,即:定義一個類來封裝資源的分配和釋放,在構造函數完成資源的分配和初始化,在析構函數完成資源的清理,可以保證資源的正確初始化和釋放。

為什麼要使用RAII?

在計算機系統中,資源是數量有限且對系統正常運行具有一定作用的元素。比如:網絡套接字、互斥鎖、文件句柄和內存等等,它們屬於系統資源。由於系統的資源是有限的,所以,我們在編程使用系統資源時,都必須遵循一個步驟:

第一步和第三步缺一不可,因為資源必須要申請才能使用的,使用完成以後,必須要釋放,如果不釋放的話,就會造成資源洩漏。

什麼是智能指針?

 所謂智能指針就是智能/自動化的管理指針所指向的動態資源的釋放。它是一個類,有類似指針的功能。 

 智能指針的實現原理?

  當類中有指針成員時,一般有兩種方式來管理指針成員:
  • 一是采用值型的方式管理,每個類對象都保留一份指針指向的對象的拷貝;
  • 另一種更優雅的方式是使用智能指針,從而實現指針指向的對象的共享。
  智能指針(smart pointer)是通用實現技術是使用引用計數(reference count)。智能指針類將一個計數器與類指向的對象相關聯,引用計數跟蹤該類有多少個對象的指針指向同一對象。

  每次創建類的新對象時,初始化指針就將引用計數置為1;當對象作為另一對象的副本而創建時,拷貝構造函數拷貝指針並增加與之相應的引用計數;對一個對象進行賦值時,賦值操作符減少左操作數所指對象的引用計數(如果引用計數為減至0,則刪除對象),並增加右操作數所指對象的引用計數;調用析構函數時,析構函數減少引用計數(如果引用計數減至0,則刪除基礎對象)。

 常見的智能指針 

包括:std::auto_ptr、boost::scoped_ptr、boost::shared_ptr、boost::scoped_array、boost::shared_array、boost::weak_ptr、boost::intrusive_ptr

Boost庫的智能指針(ps:新的C++11標准中已經引入了unique_ptr/shared_ptr/weak_ptr):


 auto_ptr                 獨占所有權,轉移所有權

 第一種實現:最開始auto_ptr的成員變量主要有T* _ptr 和 bool _owner,主要實現原理是在構造對象時賦予其管理空間的所有權,在拷貝或賦值中轉移空間的所有權,在析構函數中當_owner為true(擁有所有權)時來釋放所有權。

template<typename T>
class AutoPtr
{
public:
	//構造函數
	explicit AutoPtr(T* ptr = NULL)
		:_ptr(ptr)
		, _owner(true)
	{}
	//拷貝構造
	AutoPtr(AutoPtr<T>& ap) //參數不能寫成const的,這裡要修改ap對象的成員
:_ptr(ap._ptr) , _owner(true) { ap._owner = false; //轉讓權限 } //賦值運算符重載 AutoPtr& operator=(AutoPtr<T>& ap) { if (this! = &ap) { delete this->_ptr; this->_ptr = ap._ptr; // 轉讓權限 this->_owner = true; ap._owner = false; } return *this; } //析構函數 ~AutoPtr() { // 只刪除擁有權限的指針 if (_owner) { this->_owner = false; delete this->_ptr; } } T& operator*() { return *(this->_ptr); } T* operator->() { return this->_ptr; } T* AutoPtr<T>::GetStr() { return (this->_ptr); } protected: T* _ptr; bool _owner; //權限擁有者 };

 出現的主要問題:如果拷貝出來的對象比原來的對象先出作用域或先調用析構函數,則原來的對象的_owner雖然為false,但卻在訪問一塊已經釋放的空間,原因在於拷貝對象的釋放會導致原對象的_ptr指向的內容跟著被釋放!

問題體現如下:

ap1將析構的權限給予了ap2,由於ap1._ptr和ap2._ptr指向同一份空間,ap2在出了if作用域之後自動被釋放,進而導致ap1._ptr也被釋放。

但是在if作用域之外,又對ap1._ptr進行訪問,導致程序崩潰,這時候 ap1._ptr已經是一個野指針了,這就造成指針的懸掛的問題!

void Test()
{
		AutoPtr<int> ap1(new int(1));
		if (true)
		{
			AutoPtr<int> ap2(ap1);
		}

		//這裡的ap1._ptr已經是一個野指針了,這就造成指針的懸掛的問題
		*(ap1.GetStr() )= 10;
}

  auto_ptr的第二種實現方法:還是管理空間的所有權轉移,但這種實現方法中沒有_owner權限擁有者。構造和析構和上述實現方法類似,但拷貝和賦值後直接將_ptr賦為空,禁止其再次訪問原來的內存空間,比較簡單粗暴。

template<typename T>
class AutoPtr
{
public:
	//構造函數
	explicit AutoPtr(T* ptr = NULL)  //不能寫成const T* ptr,否則出現const類型賦值給非const類型的問題
		:_ptr(ptr)
	{}
	//拷貝構造
	AutoPtr(AutoPtr<T>& ap)
		:_ptr(ap._ptr)
	{
		ap._ptr=NULL;  
	}
	//賦值運算符重載
	AutoPtr& operator=(AutoPtr<T>& ap)
	{
		if (this! = &ap)
		{
			delete this->_ptr;
			this->_ptr = ap._ptr;
			ap._ptr = NULL;
		}
		return *this;
	}
	//析構函數
	~AutoPtr()
	{
		// 只刪除擁有權限的指針
		if (_ptr)
		{
			delete _ptr;
		}
	}

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

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

	T* GetStr()
	{
		return _ptr;
	}
protected:
	T* _ptr;
};

  這種實現方式很好的解決了舊版本野指針問題,但是由於它實現了完全的權限轉移,所以導致在拷貝構造和賦值之後只有一個指針可以使用,而其他指針都置為NULL,使用很不方便,而且還很容易對NULL指針進行解引用,導致程序崩潰,其危害也是比較大的。


 scoped_ptr         獨占所有權,防拷貝

scoped_ptr的實現原理是防止對象間的拷貝與賦值。具體實現是將拷貝構造函數和賦值運算符重載函數設置為保護或私有,並且只聲明不實現,並將標志設置為保護或私有,防止他人在類外拷貝,簡單粗暴,但是也提高了代碼的安全性。  

template<typename T>
class  ScopedPtr
{
public:
	ScopedPtr(T* ptr = NULL)  
		:_ptr(ptr)
	{}

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

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

	T* GetStr()
	{
		return _ptr;
	}
	//析構函數
	~ScopedPtr()
	{
		if (_ptr!=NULL)
		{
			delete _ptr;
		}
	}
protected:
	//防拷貝
	ScopedPtr(ScopedPtr<T>& ap);
	ScopedPtr& operator=(ScopedPtr<T>& ap);

	T* _ptr;
};

 scoped_ptr的實現和auto_ptr非常類似,不同的是 scoped_ptr有著更嚴格的使用限制——不能拷貝,這就意味著scoped_ptr 是不能轉換其所有權的。

在一般的情況下,如果不需要對於指針的內容進行拷貝,賦值操作,而只是為了防止內存洩漏的發生,scoped_ptr完全可以勝任。


 shared_ptr       共享所有權,引用計數

 shared_ptr的實現原理是通過引用計數來實現,拷貝或賦值時將引用計數加1,析構時只有當引用計數減到0才釋放空間,否則只需將引用計數減1即可.

(ps:shared_ptr在新的C++11標准中叫做unique_ptr)

template<typename T>
class SharedPtr
{
public:
	SharedPtr(T* ptr=NULL)
		:_ptr(ptr)
		, _refCount(new long(1))
	{}

	~SharedPtr()
	{
		_Release();
	}

	SharedPtr(const SharedPtr<T>& sp)
		:_ptr(sp._ptr)
		, _refCount(sp._refCount)
	{
		++(*_refCount);
	}

	//傳統寫法
	/*SharedPtr<T>& operator=(const SharedPtr<T>& sp)
	{
		if (this != &sp)
		{
			this->_Release();		
			_refCount = sp._refCount;
			_ptr = sp._ptr;
			++(*_refCount);
		}
		return *this;
	}*/

	//現代寫法
	SharedPtr<T>& operator=(SharedPtr<T> sp)
	{
		swap(_ptr, sp._ptr);
		swap(_refCount, sp._refCount);

		return *this;
	}

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

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

	T* GetPtr()
	{
		return _ptr;
	}

	long GetCount()
	{
		return *_refCount;
	}

protected:
	void _Release()
	{
		if (--(*_refCount) == 0)
		{
			//delete _ptr;
			_del(_ptr);
			delete _refCount;
		}
	}

protected:
	T* _ptr;
	long* _refCount;    //引用計數
};

  為什麼引用計數要設置為指針類型,而不設置成整型或靜態變量?  

  請看這篇文章:http://www.cnblogs.com/Lynn-Zhang/p/5400714.html

有時候看似完美的東西,往往都存在著隱含的缺陷,上面的實現仍舊存在問題!

問題如下:

第一個比較容易理解,我們可以在改變引用計數的時候加上一把互斥鎖,防止多線程帶來的隱患。

第二個,循環引用問題。我們先來看一段利用標准庫的shared_ptr 實現的代碼:

#include<iostream>
using  namespace std;
#include<memory>

struct Node
{
	shared_ptr<Node> _prev;
	shared_ptr<Node> _next;

	~Node()
	{
		cout << "delete :" <<this<< endl;
	}
};
void TestSharedPtr()
{
	shared_ptr<Node> cur(new(Node));
	shared_ptr<Node> next(new(Node));
}

  

cur和next出了作用域,都調用析構函數被釋放了。看似正確,但是如果我們加兩句代碼,問題就出現了:

void TestSharedPtr()
{
	shared_ptr<Node> cur(new(Node));
	shared_ptr<Node> next(new(Node));
	cur->_next = next;    // 1
	next->_prev = cur;    // 2
}

  

我們發現兩個節點都沒有被釋放!我們來分析一下原因:加了這兩句代碼後,這兩個節點的引用計數都增加了1。出了作用域進行析構時,兩個對象均不能釋放,因為prev的要釋放的前提是next釋放,而next的釋放又依賴於prev的釋放。最後就形成了循環引用,誰都釋放不了。

解決方案:

template<class T>
struct Node
{
public:
    ~Node()
    {
        cout << "delete:" << this << endl;
    }
public:
    weak_ptr<Node> _prev;
    weak_ptr<Node> _next;
};
 
void TestWeakPtr()
{
    shared_ptr<Node> cur(new Node());
    shared_ptr<Node> next(new Node());
 
    cout << "連接前:" << endl;
    cout << "cur:" << cur.use_count() << endl;
    cout << "next:" << next.use_count() << endl;
 
    cur->_next = next;
    next->_prev = cur;
 
    cout << "連接後:" << endl;
    cout << "cur:" << cur.use_count() << endl;
    cout << "next:" << next.use_count() << endl;
 
}

 

因為weak_ptr(弱引用智能指針)會對引用計數會做特殊處理(上述情況不加1)。

第三個,定制刪除器。在shared_ptr中只能處理釋放new開辟的空間,而對於malloc,以及fopen打開的文件指針不能處理,所以提出了定制刪除器,方便刪除其他類型的指針,而其實現則是通過仿函數(通過重載operator())來實現。

template <typename T, class Deleter = Del<T>>
class SharedPtr
{
public:
	SharedPtr(T* ptr);
	SharedPtr(T* ptr, Deleter del);
	SharedPtr(const SharedPtr<T, Deleter>& ap);
	~SharedPtr();
	//SharedPtr<T, Deleter>& operator=(const SharedPtr<T, Deleter>& ptr);//傳統寫法
	//現代寫法
	SharedPtr<T, Deleter>& operator=(SharedPtr<T, Deleter> ap);
	T& operator*()const;
	T* operator->()const;
	long GetCount()const;
	T* GetPtr()const;
protected:
	void _Realease();
protected:
	T* _ptr;
	long* _pCount;
	Deleter _del;
};

template <typename T, class Deleter = Del<T>>
SharedPtr<T, Deleter>::SharedPtr(T* ptr) :_ptr(ptr), _pCount(new long(1))
{}

template <typename T, class Deleter = Del<T>>
SharedPtr<T, Deleter>::SharedPtr(T* ptr, Deleter del) : _ptr(ptr), _pCount(new long(1)), _del(del)
{}

template <typename T, class Deleter = Del<T>>
SharedPtr<T, Deleter>::SharedPtr(const SharedPtr<T, Deleter>& ap) : _ptr(ap._ptr), _pCount(ap._pCount), _del(ap._del)
{
	++(*this->_pCount);
}

template <typename T, class Deleter = Del<T>>
SharedPtr<T, Deleter>::~SharedPtr()
{
	this->_Realease();
}

//template <typename T, class Deleter = Del<T>>//傳統寫法
//SharedPtr<T, Deleter>& SharedPtr<T, Deleter>::operator=(SharedPtr<T, Deleter> ap)
//{
//  if (this->_ptr != ap._ptr)
//  {
//      this->_Realease();
//      this->_ptr = ap._ptr;
//      this->_pCount = ap._pCount;
//      ++(*this->_pCount);
//  }
//  return *this;
//}

template <typename T, class Deleter = Del<T>>//現代寫法
SharedPtr<T, Deleter>& SharedPtr<T, Deleter>::operator=(SharedPtr<T, Deleter> ap)
{
	swap(this->_ptr, ap._ptr);
	swap(this->_pCount, ap._pCount);
	swap(this->_del, ap._del);
	return *this;
}

template <typename T, class Deleter = Del<T>>
T& SharedPtr<T, Deleter>::operator*()const
{
	return *(this->_ptr);
}

template <typename T, class Deleter = Del<T>>
T* SharedPtr<T, Deleter>::operator->()const
{
	return this->_ptr;
}

template <typename T, class Deleter = Del<T>>
long SharedPtr<T, Deleter>::GetCount()const
{
	return *(this->_pCount);
}

template <typename T, class Deleter = Del<T>>
T* SharedPtr<T, Deleter>::GetPtr()const
{
	return this->_ptr;
}

template <typename T, class Deleter = Del<T>>
void SharedPtr<T, Deleter>::_Realease()
{
	if (--(*this->_pCount) == 0)
	{
		_del(_ptr);
		delete this->_pCount;
	}
}
template<typename T>
struct Free
{
	void operator()(T* ptr)
	{	
		cout << "Free:"<<ptr<< endl;
		free(ptr);
	}
};

template<typename T>
struct Fclose
{
	void operator()(T* ptr)
	{
		cout << "Fclose:" << ptr << endl;
		fclose(ptr);
	}
};

//測試代碼
void TestDelete()
{
	int *p1 = (int*)malloc(sizeof(int));
	SharedPtr<int, Free<int>> sp1(p1);

	FILE* p2 = fopen("test.txt", "r");
	SharedPtr<FILE, Fclose<FILE>> sp2(p2);
}

  

 總結:

auto_ptr  管理權的轉移 ->不建議使用

scopd_ptr 防拷貝 ->簡單粗暴

shared_ptr 引用計數 ->增減引用計數,直到對象的引用計數為0時再釋放

scoped_array 和 shared_array 管理對象數組, 由於scopd_ptr 和 scoped_array、shared_ptr 和 shared_array 的實現都比較類似,這裡不再贅述。需要注意的是這兩種機制都重載了operator[] .

week_ptr 弱指針 ->輔助shared_ptr解決循環引用的問題。

 

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved