C++進階(智慧指標)

2023-01-03 18:00:15

智慧指標原理

C++程式設計中使用堆記憶體是非常頻繁的操作,堆記憶體的申請和釋放都由程式設計師自己管理。程式設計師自己管理堆記憶體可以提高了程式的效率,但是整體來說堆記憶體的管理是麻煩的,C++11中引入了智慧指標的概念,方便管理堆記憶體。使用普通指標,容易造成堆記憶體洩露(忘記釋放),二次釋放,程式發生異常時記憶體洩露等問題等,使用智慧指標能更好的管理堆記憶體。

從較淺的層面看,智慧指標是利用了一種叫做RAII(資源獲取即初始化)的技術對普通的指標進行封裝,這使得智慧指標實質是一個物件,行為表現的卻像一個指標。

智慧指標的作用是防止忘記呼叫delete釋放記憶體和程式異常的進入catch塊忘記釋放記憶體。另外指標的釋放時機也是非常有考究的,多次釋放同一個指標會造成程式崩潰,這些都可以通過智慧指標來解決。

智慧指標主要用於管理在堆上分配的記憶體,它將普通的指標封裝為一個棧物件。當棧物件的生存週期結束後,會在解構函式中釋放掉申請的記憶體,從而防止記憶體漏失。

智慧指標的作用是管理一個指標,因為存在以下這種情況:申請的空間在函數結束時忘記釋放,造成記憶體漏失。使用智慧指標可以很大程度上的避免這個問題,因為智慧指標是一個類,當超出了類的範例物件的作用域時,會自動呼叫物件的解構函式,解構函式會自動釋放資源。所以智慧指標的作用原理就是在函數結束時自動釋放記憶體空間,不需要手動釋放記憶體空間。

智慧指標的使用

智慧指標在C++11版本之後提供,包含在標頭檔案< memory>中:shared_ptrunique_ptrweak_ptr(注意:auto_ptr是一種存在缺陷的智慧指標,在C++11中已經被禁用了)

shared_ptr允許多個指標指向同一個物件,unique_ptr則「獨佔」所指向的物件。標準庫還定義了一種名為weak_ptr的伴隨類,它是一種弱參照,指向shared_ptr所管理的物件。

RALL

(1)基本概念
①RAII(Resource Acquisition Is Initialization)是一種利用物件生命週期來控制程式資源(如記憶體、檔案控制程式碼、網路連線、互斥量等等)的簡單技術。
在物件構造時獲取資源,接著控制對資源的存取使之在物件的生命週期內始終保持有效,最後在物件解構的時候釋放資源。藉此, 我們實際上把管理一份資源的責任託管給了一個物件 。這種做法有兩大好處

  • 不需要顯式地釋放資源
  • 採用這種方式,物件所需的資源在其生命期內始終保持有效

(2)程式碼模擬

實現智慧指標時需要考慮以下三個方面的問題:

  • 在物件構造時獲取資源,在物件解構的時候釋放資源,利用物件的生命週期來控制程式資源,即RAII特性
  • *->運運算元進行過載,使得該物件具有像指標一樣的行為
  • 智慧指標物件的拷貝問題
// RAII
// 用起來像指標一樣
template<class T>
class SmartPtr
{
public:
	SmartPtr(T* ptr)
		:_ptr(ptr)
	{}
 
	~SmartPtr()
	{
		cout << "delete:" << _ptr << endl;
		delete _ptr;
	}
 
	// 像指標一樣使用
	T& operator*()
	{
		return *_ptr;
	}
 
	T* operator->()
	{
		return _ptr;
	}
 
private:
	T* _ptr;
};

(3)為什麼要解決智慧指標物件的拷貝問題

對於當前實現的SmartPtr類,如果用一個SmartPtr物件來拷貝構造另一個SmartPtr物件,或是將一個SmartPtr物件賦值給另一個SmartPtr物件,都會導致程式崩潰

int main()
{
	SmartPtr<int> sp1(new int);
	SmartPtr<int> sp2(sp1); //拷貝構造
 
	SmartPtr<int> sp3(new int);
	SmartPtr<int> sp4(new int);
	sp3 = sp4; //拷貝賦值
	
	return 0;
}
  • 編譯器預設生成的拷貝建構函式對內建型別完成值拷貝(淺拷貝),因此用sp1拷貝構造sp2後,相當於這sp1和sp2管理了同一塊記憶體空間,當sp1和sp2解構時就會導致這塊空間被釋放兩次。
  • 編譯器預設生成的拷貝賦值函數對內建型別也是完成值拷貝(淺拷貝),因此將sp4賦值給sp3後,相當於sp3和sp4管理的都是原來sp3管理的空間,當sp3和sp4解構時就會導致這塊空間被釋放兩次,並且還會導致sp4原來管理的空間沒有得到釋放。
  • 需要注意的是,智慧指標就是要模擬原生指標的行為,當我們將一個指標賦值給另一個指標時,目的就是讓這兩個指標指向同一塊記憶體空間,所以這裡本就應該進行淺拷貝,但單純的淺拷貝又會導致空間被多次釋放,因此根據解決智慧指標拷貝問題方式的不同,從而衍生出了不同版本的智慧指標。

unique_ptr

原理和使用

unique_ptr「唯一」擁有其所指物件,同一時刻只能有一個unique_ptr指向給定物件(通過禁止拷貝語意、只有移動語意來實現)。它對於避免資源洩露(例如「以new建立物件後因為發生異常而忘記呼叫delete」)特別有用。

相比與原始指標,unique_ptr用於其RAII的特性,使得在出現異常的情況下,動態資源能得到釋放。unique_ptr指標本身的生命週期:從unique_ptr指標建立時開始,直到離開作用域。離開作用域時,若其指向物件,則將其所指物件銷燬(預設使用delete操作符,使用者可指定其他操作)。

unique_ptr指標與其所指物件的關係:在智慧指標生命週期內,可以改變智慧指標所指物件,如建立智慧指標時通過建構函式指定、通過reset方法重新指定、通過release方法釋放所有權、通過移動語意轉移所有權。
範例:

#include <iostream>
#include <memory>

int main() {
    {
        std::unique_ptr<int> uptr(new int(10));  //繫結動態物件
        //std::unique_ptr<int> uptr2 = uptr;  //不能賦值
        //std::unique_ptr<int> uptr2(uptr);  //不能拷貝
        std::unique_ptr<int> uptr2 = std::move(uptr); //轉換所有權
        uptr2.release(); //釋放所有權
    }
    //超過uptr的作用域,記憶體釋放
}

說明:C++有一個標準庫函數move(),讓你能夠將一個unique_ptr賦給另一個。儘管轉移所有權後還是有可能出現原有指標呼叫(呼叫就崩潰)的情況。但是這個語法能強調你是在轉移所有權,讓你清晰的知道自己在做什麼,從而不亂呼叫原有指標。

模擬實現

namespace XM
{
	template<class T>
	class unique_ptr
	{
	public:
		unique_ptr(T* ptr = nullptr)
			: _ptr(ptr)
		{}
 
		~unique_ptr()
		{
			if (_ptr)
				delete _ptr;
		}
 
		T& operator*() { return *_ptr; }
		T* operator->() { return _ptr; }
 
	private:
		// C++98防拷貝的方式:只宣告不實現+宣告成私有
		unique_ptr(const unique_ptr<T>& sp);
		unique_ptr& operator=(const unique_ptr<T>& sp);
 
		// C++11防拷貝的方式:delete
		unique_ptr(const unique_ptr<T>& sp) = delete;
		unique_ptr& operator=(const unique_ptr<T>& sp) = delete;
 
	private:
		T* _ptr;
	};
}

share_ptr

原理和使用

C++ 11中最常用的智慧指標型別為shared_ptr。從名字share就可以看出了資源可以被多個指標共用,它使用計數機制來表明資源被幾個指標共用。可以通過成員函數use_count()來檢視資源的所有者個數。除了可以通過new來構造,還可以通過傳入auto_ptr, unique_ptr, weak_ptr來構造。當我們呼叫release()時,當前指標會釋放資源所有權,計數減一。當計數等於0時,資源會被釋放。

  • shared_ptr的原理:是通過參照計數的方式來實現多個shared_ptr物件之間共用資源。
  • shared_ptr在其內部,給每個資源都維護了著一份計數,用來記錄該份資源被幾個物件共用。
  • 物件被銷燬時(也就是解構函式呼叫),就說明自己不使用該資源了,物件的參照計數減一。
  • 如果參照計數是0,就說明自己是最後一個使用該資源的物件,必須釋放該資源如果不是0,就說明除了自己還有其他物件在使用該份資源,不能釋放該資源,否則其他物件就成野指標了

注意事項:

  • 初始化。智慧指標是個模板類,可以指定型別,傳入指標通過建構函式初始化。也可以使用make_shared函數初始化。不能將指標直接賦值給一個智慧指標,一個是類,一個是指標。例如:std::shared_ptr< int> p4 = new int(1);的寫法是錯誤的!

  • 拷貝和賦值。拷貝使得物件的參照計數增加1,賦值使得原物件參照計數減1,當計數為0時,自動釋放記憶體。後來指向的物件參照計數加1,指向後來的物件。

  • get函數獲取原始指標。

  • 不要用一個原始指標初始化多個shared_ptr,否則會造成二次釋放同一記憶體。

  • 避免迴圈參照。shared_ptr的一個最大的陷阱是迴圈參照,迴圈參照會導致堆記憶體無法正確釋放,導致記憶體漏失。迴圈參照在weak_ptr中介紹。

成員函數:

  • use_count 返回參照計數的個數;
  • unique 返回是否是獨佔所有權(use_count 為 1);
  • swap 交換兩個 shared_ptr 物件(即交換所擁有的物件);
  • reset 放棄內部物件的所有權或擁有物件的變更, 會引起原有物件的參照計數的減少;
  • get 返回內部物件(指標), 由於已經過載了()方法, 因此和直接使用物件是一樣的。

範例:

class A
{
public:
	int _a = 10;
	~A()
	{
		cout << "~A()" << endl;
	}
};

void test()
{
	shared_ptr<A> sp(new A);
	shared_ptr<A> sp2(new A);
	shared_ptr<A> sp3(sp2);//ok
	sp3 = sp;//ok
	sp->_a = 100;
	sp2->_a = 1000;
	sp3->_a = 10000;
	cout << sp->_a << endl;
	cout << sp2->_a << endl;
	cout << sp3->_a << endl;
}

執行結果如下:

我們發現申請多少資源就會釋放多少資源,此時的sp和sp3共用一份資源,修改sp3也就相等於修改了sp。所以最終都會列印10000。那共用了一份資源,是如何實現資源只釋放一次呢?----參照計數

我們可以通過shared_ptr提供的介面use_count()來檢視,當前有多少個智慧指標來管理同一份資源

void test()
{
	shared_ptr<A> sp(new A);
	cout << sp.use_count() << endl;//1
	shared_ptr<A> sp2(sp);
	cout << sp.use_count() << endl;//2
	cout << sp2.use_count() << endl;//2
	shared_ptr<A> sp3(new A);
	cout << sp.use_count() << endl;//2
	cout << sp2.use_count() << endl;//2
	cout << sp3.use_count() << endl;//1
	sp3 = sp;
	sp3 = sp2;
	cout << sp.use_count() << endl;//3
	cout << sp2.use_count() << endl;//3
	cout << sp3.use_count() << endl;//3
}

執行截圖:之所以中間會有調解構函式,是因為當sp3指向sp時,sp3的參照計數為0,則會呼叫解構函式來釋放資源。此時sp建立的資源就有3個指智慧指標來管理

圖解:

在實現時,我們應該確保一個資源只對應一個計數器,而不是每個智慧指標都有各自的計數器。所以我們可以將資源和計數器繫結在一起,此時指向同一份資源的智慧指標,存取的也都是同一個計數器(後面會解釋)

模擬實現

  • 在shared_ptr類中增加一個成員變數count,表示智慧指標物件管理的資源對應的參照計數。
  • 在建構函式中獲取資源,並將該資源對應的參照計數設定為1,表示當前只有一個物件在管理這個資源。
  • 在拷貝建構函式中,與傳入物件一起管理它管理的資源,同時將該資源對應的參照計數++。
  • 在拷貝賦值函數中,先將當前物件管理的資源對應的參照計數--(如果減為0則需要釋放),然後再與傳入物件一起管理它管理的資源,同時需要將該資源對應的參照計數++。
  • 在解構函式中,將管理資源對應的參照計數--,如果減為0則需要將該資源釋放。
  • 對*和->運運算元進行過載,使shared_ptr物件具有指標一樣的行為。
namespace XM
{
template<class T>
class shared_ptr
{
public:
	shared_ptr(T* ptr)
		:_ptr(ptr)
		, _pRefCount(new int(1))
	{}
 
	shared_ptr(const shared_ptr<T>& sp)
		:_ptr(sp._ptr)
		, _pRefCount(sp._pRefCount)
	{
		++(*_pRefCount);
	}
 
	shared_ptr<T>& operator=(const shared_ptr<T>& sp)
	{
		if (_ptr != sp._ptr)
		{
			if (--(*_pRefCount) == 0)
			{
				delete _ptr;
				delete _pRefCount;
			}
 
			_ptr = sp._ptr;
			_pRefCount = sp._pRefCount;
			++(*_pRefCount);
		}
 
		return *this;
	}
 
	~shared_ptr()
	{
		if (--(*_pRefCount) == 0 && _ptr)
		{
			cout << "delete:" << _ptr << endl;
			delete _ptr;
			delete _pRefCount;
 
			//_ptr = nullptr;
			//_pRefCount = nullptr;
		}
	}
    
    int use_count() const
    {
       return *_pRefCount;
    }
 
	// 像指標一樣使用
	T& operator*()
	{
		return *_ptr;
	}
 
	T* operator->()
	{
		return _ptr;
	}
private:
	T* _ptr;
	int* _pRefCount;
};
}

思考一個問題:為什麼參照計數要放在堆區?★

  • ①首先,shared_ptr中的參照計數count不能單純的定義成一個int型別的成員變數,因為這就意味著每個shared_ptr物件都有一個自己的count成員變數,而當多個物件要管理同一個資源時,這幾個物件應該用到的是同一個參照計數。
  • ②其次,shared_ptr中的參照計數count也不能定義成一個靜態的成員變數,因為靜態成員變數是所有型別物件共用的,這會導致管理相同資源的物件和管理不同資源的物件用到的都是同一個參照計數。
  • ③而如果將shared_ptr中的參照計數count定義成一個指標,當一個資源第一次被管理時就在堆區開闢一塊空間用於儲存其對應的參照計數,如果有其他物件也想要管理這個資源,那麼除了將這個資源給它之外,還需要把這個參照計數也給它。
  • ④這時管理同一個資源的多個物件存取到的就是同一個參照計數,而管理不同資源的物件存取到的就是不同的參照計數了,相當於將各個資源與其對應的參照計數進行了繫結。
  • ⑤但同時需要注意,由於參照計數的記憶體空間也是在堆上開闢的,因此當一個資源對應的參照計數減為0時,除了需要將該資源釋放,還需要將該資源對應的參照計數的記憶體空間進行釋放。

執行緒安全問題

我們實現的shared_ptr智慧指標在多執行緒的場景下其實是存線上程安全問題的----參照計數器指標是一個共用變數,多個執行緒進行修改時會導致計數器混亂。導致資源提前被釋放或者會產生記憶體漏失問題
我們來看看一下程式碼

#include<iostream>
#include<memory>
#include<mutex>
#include<thread>
 
using namespace std;
 
 
struct Date
{
	int _year = 0;
	int _month = 0;
	int _day = 0;
};
 
 
namespace XM
{
 
	template<class T>
	class shared_ptr
	{
	public:
		shared_ptr(T* ptr)
			:_ptr(ptr)
			, _pRefCount(new int(1))
		{}
 
		shared_ptr(const shared_ptr<T>& sp)
			:_ptr(sp._ptr)
			, _pRefCount(sp._pRefCount)
		{
			AddRef();
		}
 
		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			if (_ptr != sp._ptr)
			{
				Release();
 
				_ptr = sp._ptr;
				_pRefCount = sp._pRefCount;
				AddRef();
 
			}
 
			return *this;
		}
 
 
 
		~shared_ptr()
		{
			Release();
		}
 
		T* get() const
		{
			return _ptr;
		}
 
		int use_count()
		{
			return *_pRefCount;
		}
 
		
		T& operator*()
		{
			return *_ptr;
		}
 
		T* operator->()
		{
			return _ptr;
		}
 
	private:
		void Release()
		{
			if (--(*_pRefCount) == 0 && _ptr)
			{
				delete _ptr;
				delete _pRefCount;
			}
		}
 
		void AddRef()  //增加計數
		{
			++(*_pRefCount);
		}
 
	private:
		T* _ptr;
		int* _pRefCount;
	};
}
 
 
 
void SharePtrFunc(XM::shared_ptr<Date>& sp, size_t n,mutex& mtx)
{
	cout << sp.get() << endl;
 
	for (size_t i = 0; i < n; ++i)
	{
		// 這裡智慧指標拷貝會++計數,智慧指標解構會--計數,自己模擬實現是不安全的
		XM::shared_ptr<Date> copy(sp);
 
        
		{
			unique_lock<mutex> lk(mtx);
			copy->_year++;
			copy->_month++;
			copy->_day++;
		}
 
	}
}
 
int main()
{
	XM::shared_ptr<Date> p(new Date);
	cout << p.get() << endl;
	const size_t n = 10000;
	mutex mtx;
 
	thread t1(SharePtrFunc, std::ref(p), n,std::ref(mtx));
	thread t2(SharePtrFunc, std::ref(p), n,std::ref(mtx));
 
	t1.join();
	t2.join();
 
	cout << p->_year << endl;
	cout << p->_month << endl;
	cout << p->_day << endl;
 
	cout << p.use_count() << endl;
 
	return 0;
}
  • ①通過實驗結果可知,如果share_ptr不加鎖在多執行緒的情況下是不安全的,在pRefCount ++,- - 時 可能出現錯誤
  • ②智慧指標物件中參照計數是多個智慧指標物件共用的,兩個執行緒中智慧指標的參照計數同時++或--,這個操作不是原子的,參照計數原來是1,++了兩次,可能還是2.這樣參照計數就錯亂了。會導致資源未釋放或者程式崩潰的問題。所以只能指標中參照計數++、--是需要加鎖的,也就是說參照計數的操作是執行緒安全的。
  • ③智慧指標管理的物件存放在堆上,兩個執行緒中同時去存取,會導致執行緒安全問題
  • ④這裡智慧指標存取管理的資源,不是執行緒安全的;對Date的成員 ++ , 所以我們看看這些值兩個執行緒++了2n次,但是最終看到的結果,並一定是加了2n ; 為了保證執行緒安全還要手動加鎖

shared_ptr智慧指標是執行緒安全的嗎?

  • 是的,參照計數的加減是加鎖保護的。但是指向的資源不是執行緒安全的,需要自己管
  • 指向堆上資源的執行緒安全問題是存取的人處理的,智慧指標不管,也管不了; 參照計數的執行緒安全問題,是智慧指標要處理的

模擬執行緒安全的程式碼 , 參照計數加鎖
①要解決參照計數的執行緒安全問題,本質就是要讓對參照計數的自增和自減操作變成一個原子操作,因此可以對參照計數的操作進行加鎖保護,也可以用原子類atomic對參照計數進行封裝,這裡以加鎖為例

  • 在shared_ptr類中新增互斥鎖成員變數,為了讓管理同一個資源的多個執行緒存取到的是同一個互斥鎖,管理不同資源的執行緒存取到的是不同的互斥鎖,因此互斥鎖也需要在堆區建立
  • 在呼叫拷貝建構函式和拷貝賦值函數時,除了需要將對應的資源和參照計數交給當前物件管理之外,還需要將對應的互斥鎖也交給當前物件。
  • 當一個資源對應的參照計數減為0時,除了需要將對應的資源和參照計數進行釋放,由於互斥鎖也是在堆區建立的,因此還需要將對應的互斥鎖進行釋放。
  • 為了簡化程式碼邏輯,可以將拷貝建構函式和拷貝賦值函數中參照計數的自增操作提取出來,封裝成AddRef函數,將拷貝賦值函數和解構函式中參照計數的自減操作提取出來,封裝成Release函數,這樣就只需要對AddRef和Release函數進行加鎖保護即可。
namespace XM
{
 
	template<class T>
	class shared_ptr
	{
	public:
		shared_ptr(T* ptr)
			:_ptr(ptr)
			, _pRefCount(new int(1))
			,_pmtx(new mutex)
		{}
 
		shared_ptr(const shared_ptr<T>& sp)
			:_ptr(sp._ptr)
			, _pRefCount(sp._pRefCount)
			,_pmtx(sp._pmtx)
		{
			AddRef();
		}
 
		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			//if (this != &sp) 這樣判斷不太好,防止自己給自己賦值應該判斷指標的值是否相同
			if (_ptr != sp._ptr)
			{
				Release();
 
				_ptr = sp._ptr;
				_pRefCount = sp._pRefCount;
				_pmtx = sp._pmtx;
				AddRef();
 
			}
 
			return *this;
		}
 
 
 
		~shared_ptr()
		{
			Release();
		}
 
		T* get() const
		{
			return _ptr;
		}
 
		int use_count()
		{
			return *_pRefCount;
		}
 
 
		T& operator*()
		{
			return *_ptr;
		}
 
		T* operator->()
		{
			return _ptr;
		}
 
	private:
		void Release() //釋放資源
		{
			_pmtx->lock();
			bool flag = false;
			if (--(*_pRefCount) == 0 && _ptr)
			{
				delete _ptr;
				delete _pRefCount;
 
				flag = true;  //鎖不能在這裡釋放,因為後面要解鎖
			}
			_pmtx->unlock();
 
			if (flag == true)
			{
				delete _pmtx;
			}
		}
 
		void AddRef()  //增加計數
		{
			_pmtx->lock();
 
			++(*_pRefCount);
 
			_pmtx->unlock();
		}
 
	private:
		T* _ptr;
		int* _pRefCount;
		mutex* _pmtx;
	};
}

小結:

  • 在Release函數中,當參照計數被減為0時需要釋放互斥鎖資源,但不能在臨界區中釋放互斥鎖,因為後面還需要進行解鎖操作,因此程式碼中藉助了一個flag變數,通過flag變數來判斷解鎖後釋放需要釋放互斥鎖資源。
  • shared_ptr只需要保證參照計數的執行緒安全問題,而不需要保證管理的資源的執行緒安全問題,就像原生指標管理一塊記憶體空間一樣,原生指標只需要指向這塊空間,而這塊空間的執行緒安全問題應該由這塊空間的操作者來保證

迴圈參照

shared_ptr其實也存在一些小問題,也就是迴圈參照問題

#include<iostream>
#include<memory>
#include<string>
using namespace std;
class A;
class B;
class A {
public:
	shared_ptr<B> bptr;
	~A()
	{
		cout << "class Ta is disstruct" << endl;
	}
};
class B {
public:
	shared_ptr<A>aptr;
	~B()
	{
		cout << "class Tb is disstruct" << endl;
	}
};
void testPtr()
{
	shared_ptr<A>ap(new A);
	shared_ptr<B>bp(new B);
	cout << "ap的參照計數" << ap.use_count() << endl;//ap的參照計數1
	cout << "bp的參照計數" << bp.use_count() << endl;//bp的參照計數1
	ap->bptr = bp;
	bp->aptr = ap;
	cout << "ap的參照計數" << ap.use_count() << endl;//ap的參照計數2
 
 
	cout << "bp的參照計數" << bp.use_count() << endl;//bp的參照計數2
}
int main()
{
	testPtr();
	return 0;
}

我們可以用圖來理解一下上述程式智慧指標參照關係:

共用智慧指標ap指向A的範例物件,記憶體參照計數+1,B的範例物件裡面的成員aptr被ap賦值,所以aptr與ap共同指向同一塊記憶體,該記憶體參照計數變為2;同理指向B物件的也有兩個共用智慧指標,其參照計數也為2。

當函數結束時,ap,bp兩個共用智慧指標離開作用域,參照計數均減為1,在這種情況下不會刪除智慧指標所管理的記憶體,導致A,B的範例物件不能被解構,最終造成記憶體漏失,如圖:

迴圈參照的解決方式 weak_ptr

share_ptr雖然已經很好用了,但是有一點share_ptr智慧指標還是有記憶體洩露的情況,當兩個物件相互使用一個shared_ptr成員變數指向對方,會造成迴圈參照,使參照計數失效,從而導致記憶體漏失。

weak_ptr是為了配合shared_ptr而引入的一種智慧指標,因為它不具有普通指標的行為,沒有過載operator*和->,它的最大作用在於協助shared_ptr工作,像旁觀者那樣觀測資源的使用情況。weak_ptr可以從一個shared_ptr或者另一個weak_ptr物件構造,獲得資源的觀測權。但weak_ptr沒有共用資源,它的構造和解構不會引起參照記數的增加或減少。

weak_ptr是用來解決shared_ptr相互參照時的死鎖問題,如果說兩個shared_ptr相互參照,那麼這兩個指標的參照計數永遠不可能下降為0,資源永遠不會釋放。它是對物件的一種弱參照,不會增加物件的參照計數,和shared_ptr之間可以相互轉化,shared_ptr可以直接賦值給它,它可以通過呼叫lock函數來獲得shared_ptr。

使用weak_ptr的成員函數use_count()可以觀測資源的參照計數,另一個成員函數expired()的功能等價於use_count()0,但更快,表示被觀測的資源(也就是shared_ptr的管理的資源)已經不復存在。weak_ptr可以使用一個非常重要的成員函數lock()從被觀測的shared_ptr獲得一個可用的shared_ptr物件,從而操作資源。但當expired()true的時候,lock()函數將返回一個儲存空指標的shared_ptr。
範例:

#define _CRT_SECURE_NO_WARNINGS
#include"bitset.h"
#include<memory>
int main() {
    shared_ptr<int> sh_ptr = make_shared<int>(10);
    cout << sh_ptr.use_count() << endl;//1

    weak_ptr<int> wp(sh_ptr);
    cout << wp.use_count() << endl;//1

    if (!wp.expired()) {
        shared_ptr<int> sh_ptr2 = wp.lock(); //get another shared_ptr
        *sh_ptr = 100;
        cout << wp.use_count() << endl;//2
    }

//delete memory

	system("pause");
	return EXIT_SUCCESS;
}

客製化刪除器

關於new和delete的補充

  • 如果A的解構函式沒有顯示寫,這裡不會報錯也不會有記憶體漏失,原因: new底層是用malloc開闢空間,delete底層是free,free不管你開闢多少空間,開多少釋放多少空間
  • 如果A的解構函式顯示寫,這裡就會出問題,原因 : new的時候如果有解構函式的情況下,假設一個物件是4位元組,10個物件是40個位元組,它不會只開40個位元組,它還要在頭部多開4個位元組去存物件的個數,delete的時候,delete[]沒有指明delete幾個物件,它去頭部取那4個位元組,發現是10就呼叫10次解構函式

客製化刪除器的用法

(1)錯誤用法

  • 當智慧指標物件的生命週期結束時,所有的智慧指標預設都是以 delete 的方式將資源釋放,這是不太合適的,因為智慧指標並不是只管理以 new 方式申請到的記憶體空間,智慧指標管理的也可能是以 new[ ] 的方式申請到的空間,或管理的是一個檔案指標
  • 這時當智慧指標物件的生命週期結束時,再以 delete 的方式釋放管理的資源就會導致程式崩潰,因為以 new[ ] 的方式申請到的記憶體空間必須以 delete[ ] 的方式進行釋放,而檔案指標必須通過呼叫 fclose 函數進行釋放
struct ListNode
{
	ListNode* _next;
	ListNode* _prev;
	int _val;
	~ListNode()
	{
		cout << "~ListNode()" << endl;
	}
};
 
int main()
{
	std::shared_ptr<ListNode> sp1(new ListNode[10]);   //error
	std::shared_ptr<FILE> sp2(fopen("test.cpp", "r")); //error
 
	return 0;
}

(2)正確用法

我們來看 C++ 是如何解決的

unique_ptr類別範本原型:

//non-specialized	
template <class T, class D = default_delete<T>>
class unique_ptr;
//array specialization	
template <class T, class D>
class unique_ptr<T[],D>;

可以看到,這裡提供了一個模板引數 class D = default_delete<T> ,這就是刪除器,它支援傳入仿函數型別,可以由我們自己客製化。

shared_ptr類別範本原型:

template <class U, class D>
class unique_ptr<U* p ,D del>;

①引數

  • p:需要讓智慧指標管理的資源。
  • del:刪除器,這個刪除器是一個可呼叫物件,比如函數指標、仿函數、lambda表示式以及被包裝器包裝後的可呼叫物件。

②當shared_ptr物件的生命週期結束時就會呼叫傳入的刪除器完成資源的釋放,呼叫該刪除器時會將shared_ptr管理的資源作為引數進行傳入

③因此當智慧指標管理的資源不是以 new 的方式申請到的記憶體空間時,就需要在構造智慧指標物件時傳入客製化的刪除器

template<class T>
struct DelArr
{
	void operator()(const T* ptr)
	{
		cout << "delete[]: " << ptr << endl;
		delete[] ptr;
	}
};
 
int main()
{
	std::shared_ptr<ListNode> sp1(new ListNode[10], DelArr<ListNode>()); //仿函數
	std::shared_ptr<FILE> sp2(fopen("test.cpp", "r"), [](FILE* ptr){
		cout << "fclose: " << ptr << endl;
		fclose(ptr);
	}); //lamba表示式
 
	return 0;
}

小結

  • 客製化刪除器,實際在平時的工作中使用有價值
  • 客製化刪除器的意義 : 預設情況,智慧指標底層都是delete資源 ,那麼如果你的資源不是new出來的呢?比如:new[]、malloc、fopen ,客製化刪除器 -- 傳入可呼叫物件,自定義釋放資源