C++之智能指針

遇到的問題:

  在編寫C++程序時,我們最常遇到的問題也就是內存方面的問題了。申請內存後未釋放,打開文件後未關閉,這些都屬於內存泄漏的問題。
  舉個栗子:
如果我們在申請內存之後,程序拋了一個異常,並且我們的catch代碼段也沒有去做對應的處理,那麼這個時候就會發生內存泄漏。如下程序:

int Division(int a, int b)
{
	if (b == 0) {
		throw "Division by zero condition";
	}
	return a / b;
}

int main()
{
	try
	{
		int *str = new int[1000];	//申請內存
		int a = 10;
		int b = 0;
		cout << Division(a, b) << endl;		//拋異常
		delete[] str;		//釋放內存語句未執行
	}
	catch (const char* e)
	{
		cout << e << endl;	//沒有對內存進行釋放,導致內存泄漏
	}
	return 0;
}

上面這段代碼可以看到在 try 代碼段中我們申請了一塊空間,但是當我們運行到Division 函數中時,它判斷除數是 0 ,拋出異常,那麼程序接下來會跳過後面的 delete[ ] str;而且我們也沒有在 catch 代碼塊中進行釋放內存的操作,因此會造成內存泄漏。

智能指針:

RAII:

  RAII 是一種利用對象生命週期來控制內存資源的一種技術,因爲在面向對象的編程語言中,對象的創建和銷燬分別是通過構造函數和析構函數來完成的,而且這兩個函數都不用程序猿人爲的控制,都是由系統自動調用的。
  我們可以將資源的申請放在構造函數中,將資源的釋放放在析構函數中,這樣即便程序異常退出,在對象生命結束的時候,系統也會自動調用析構函數去執行釋放資源的語句。這樣大大減少了內存泄漏一系列問題的產生。
  RAII帶來的好處:
1、省去了我們人爲釋放資源的過程。
2、資源在對象生命週期內始終有效。

智能指針的實現:

原理:RAII 的思想 + 實現指針的特徵(重載 *、->的操作)

std::auto_ptr:
auto_ptr 是 C++98版本就提供的一款智能指針。
auto_ptr 的問題:管理權轉移,當對象發生拷貝或者賦值之後,前面對象就會懸空,導致前面的智能指針不能再用。
模擬實現:
下面的所有代碼中的 A 類如下:

class A
{
public:
	A()
	{ cout << "A()" << endl; }
	~A()
	{ cout << "~A()" << endl; }
	int _num;
};

auto_ptr 模擬實現:

template <class T>
class AutoPtr
{
public:
	AutoPtr(T* ptr = nullptr)
		:_ptr(ptr)
	{}

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

	//拷貝
	AutoPtr(AutoPtr& ap)
	{
		_ptr = ap._ptr;
		ap._ptr = nullptr;		//資源轉移,管理權移交,ap的管理權失效
	}

	//賦值
	AutoPtr<T>& operator=(AutoPtr<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;
};

演示拷貝/賦值管理權轉移:
在這裏插入圖片描述
std::unique_ptr:
爲了解決這個管理權轉移的問題,在 C++11 中引入了一個新的智能指針 unique_ptr,這個智能指針的做法非常粗暴,那麼就是直接不讓拷貝和賦值。
所以它的問題就是不能拷貝和賦值。
模擬實現:

template <class T>
class UniquePtr
{
public:
	UniquePtr()
		:_ptr(nullptr)
	{}
	~UniquePtr()
	{
		if(_ptr)
			delete _ptr;
	}
	T* operator->()
	{ return _ptr; }

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

不能賦值和拷貝肯定是不合理的,所以C++11又有了shared_ptr來解決這些問題。
std::shared_ptr:
原理:通過引用計數來解決不能拷貝和賦值的問題。
1、shared_ptr在其內部給每一份資源都維護了一個引用計數,通過引用計數來統計當前資源被幾個對象共享。
2、在對象被銷燬時,這份資源並不是直接釋放,而是該資源的引用計數 -1 。直到引用計數減爲 0,證明現在資源沒有對象在使用,資源纔會被釋放。

問題:循環引用

模擬實現:(在模擬實現的過程中要注意,因爲引用計數是被多個對象所共享的,所以在對引用計數進行 ++操作或者 --操作時要注意線程安全問題,要通過加鎖來實現線程安全)

template <class T>
class SharedPtr
{
public:
	SharedPtr(T* ptr = nullptr)
		:_ptr(ptr)
		,_mutex(new mutex)
		,_Count(new int(1))
	{}
	~SharedPtr()
	{
		Release();
	}

	SharedPtr(const SharedPtr<T>& sp)
		:_ptr(sp._ptr)
		, _mutex(sp._mutex)
		, _Count(sp._Count)
	{
		AddRefCount();
	}

	SharedPtr<T>& operator=(const SharedPtr<T>& sp)
	{
		if (_ptr != sp._ptr) {
			Release();

			_ptr = sp._ptr;
			_mutex = sp._mutex;
			_Count = sp._Count;

			AddRefCount();
		}
		return *this;
	}

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

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

	int UseCount()
	{
		return *_Count;
	}

	void AddRefCount()
	{
		_mutex->lock();
		++(*_Count);
		_mutex->unlock();
	}
private:
	void Release()
	{
		bool flag = false;	//計數爲0標誌位
		_mutex->lock();
		if (--(*_Count) == 0) {
			delete _ptr;
			delete _Count;
			flag = true;
		}
		_mutex->unlock();

		if (flag == true)
			delete _mutex;
	}
private:
	T* _ptr;	//指向管理資源的指針
	mutex* _mutex;	//互斥鎖
	int* _Count;	//引用計數
};

上面提到shared_ptr有一個問題:循環引用。
我們看看下面這種情況:

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

int main()
{
	shared_ptr<ListNode> node1(new ListNode);
	shared_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;
	return 0;
}

運行結果如圖:
在這裏插入圖片描述
看起來好像沒什麼問題,在沒有指向新的資源時,node1 和 node2 的引用計數都是 1. 指向新的資源後,引用計數變爲 2。但是按照邏輯,程序執行完應該調用析構函數釋放對象資源纔對,這裏並沒有調用析構函數,這就是循環引用問題,這個問題導致了令人談虎色變的內存泄漏問題。

我們將相互指向的兩行代碼註釋掉,看看運行結果:
在這裏插入圖片描述
這樣程序是走的正常的邏輯,當有對象釋放資源時,引用計數 -1,引用計數減爲 0 時,最終調用析構函數,資源徹底釋放。
所以應該如何解決循環引用的問題呢?
主要有以下兩種辦法:
① 當要發生循環引用的時候,手動打破循環引用釋放對象資源
② 使用弱引用指針 weak_ptr 來打破循環引用

第一種方法較爲麻煩,所以第二種方法是最常用的方法。
將我們剛纔的代碼進行修改,將ListNode中的強引用智能指針改爲弱引用智能指針。也就是說在有可能發生循環引用的地方都是用弱引用智能指針。

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

在這裏插入圖片描述
修改之後循環引用的問題也就迎刃而解了。


boost::scoped_ptr:
scoped_ptr 爲了解決管理權轉移問題也是非常簡單粗暴直接防拷貝防賦值。

在boost庫中有很多優秀的東西,比如 C++11 標準庫中的uniqued_ptr、weak_ptr、shared_ptr都是參照boost庫中的實驗原理實現的。
在 boost 庫中有一個 scoped_ptr ,在C++11標準庫中的 uniqued_ptr 就是對應 boost 庫中的scoped_ptr。

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