智慧指標思想實踐(std::unique_ptr, std::shared_ptr)

2022-07-09 21:01:36

1 smart pointer 思想

​ 個人認為smart pointer實際上就是一個對原始指標型別的一個封裝類,並對外提供了-> 和 * 兩種操作,使得其能夠表現出原始指標的操作行為。

​ 要理解smart pointer思想首先要了解一個概念RAII(Resource Acquisition Is Initialization), 直譯為資源獲取即初始化,核心理念為在物件建立時分配資源,而在物件銷燬時釋放資源.

​ 根據RAII理念,如果物件建立在棧(stack)上,由於棧上的物件在銷燬是會自動呼叫解構函式,因此僅僅需要在建構函式內完成資源分配,而在解構函式內完成資源釋放,此時程式設計師就不需要自己關心資源的釋放問題。

​ 但當物件建立在自由儲存區(free store)上時,例如:

class Fruit {
public:
    Fruit(std::string name = "fruit", int num = 1) :name_{ name }, num_{ num }{}
	~Fruit(){ cout << "destroy fruit" << endl;}
	std::string name_;
	int num_;
};

int main(){
    Fruit* intPtr{new Fruit};//memory leak
	return 0;
}

此時系統僅僅能回收在棧上1建立的指標intPtr所佔據的資源,對於指標所指向的動態分配的記憶體空間並不會自動呼叫解構函式進行資源釋放,此時如果程式設計師不主動呼叫 delete 進行資源釋放則會產生記憶體漏失

​ 那麼如何讓建立在自由儲存區的物件也能夠自動地釋放資源,而不需要程式設計師自己手動釋放資源呢?智慧指標給出了一種非常巧妙的解決思路,它將一個原本定義在自由儲存區的物件封裝進了一個建立在棧上的資源管理物件中,由這個資源管理物件在自己的解構函式中釋放定義在自由儲存區上的物件所佔據的資源。這使得程式設計師只需要利用資源管理物件接管在自由儲存區上動態建立的物件資源,利用棧物件的生存機制能夠實現資源的自動釋放而不需要自己手動delete 物件資源。例如:

template <typename T>
class ResourceManager {
public:
	ResourceManager(T* ptr) :ptr_{ ptr } {}
	~ResourceManager() {
		cout << "delete arr in free store" << endl;
		delete ptr_;
	}

private:
	T* ptr_;
};

void AutoManage(){
    ResourceManager fruit{ new Fruit};
}
    
int main(){
    AutoManage();//delete arr in free store
    system("pause");
	//cout << fruit->name_ << " " << (*fruit).num_ << endl;//fruit 1
	return 0;
}

在AutoManage()函數中動態分配一個Fruit物件,並將其封裝進ResourceManager資源管理類中,當程式離開函數AutoManage()時,由於ResourceManager是一個定義在棧上的物件,程式會自動呼叫解構函式~ResourceManager()進行物件銷燬操,此時由於ResourceManager在解構函式中進行了Fruit資源的釋放,因此不會發生記憶體漏失問題,一次不需要程式設計師手動釋放資源的自動記憶體管理過程完美完成。

​ 以上僅僅完成了動態分配的資源的自動回收功能,要使得ResourceManager資源管理類能夠像Fruit*指標一樣操作Fruit物件的成員,還需要對外提供***** 以及->兩種指標操作:

template <typename T>
class ResourceManager {
public:
	ResourceManager(T* ptr) :ptr_{ ptr } {}
	~ResourceManager() {
		cout << "delete arr in free store" << endl;
		delete ptr_;
	}
	T*& operator->() {return ptr_;}
	T& operator*() { return *ptr_; }

private:
	T* ptr_;
};

void AutoManage(){
    ResourceManager fruit{ new Fruit};
}
    
int main(){
    AutoManage();//delete arr in free store
    system("pause");
	cout << fruit->name_ << " " << (*fruit).num_ << endl;//fruit 1
	return 0;
}

此時可以利用ResourceManager提供的***** 以及->操作符直接操作原始Fruit* 指標,使得ResourceManager物件就像一個真實的指向Fruit物件的Fruit* 指標。

2 unique_ptr 思想

unique_ptr作為最常用的智慧指標,它提供了對資源的獨佔式管理,即對資源的唯一所有權(sole ownership), 這就要求unique_ptr是一個不可複製的物件。每一個unique_ptr物件都有義務對其管理的資源進行釋放。但unique_ptr 並不限制移動(move)操作所導致的所有權轉移。最後不要忘記unique_ptr作為一個智慧指標概念,它必須能夠自動管理動態分配的物件資源,並且提供對物件資源的指標操作。概括一下,unique_ptr要求:

  1. 不可複製
  2. 能夠移動
  3. 自動記憶體管理
  4. 指標操作
template<typename T>
class UniquePtr {
public:
	UniquePtr(T* ptr):ptr_{ptr}{}
	~UniquePtr() {
		cout << "delete unique resource in free store" << endl;
		delete ptr_;//釋放資源
	}
	UniquePtr(const UniquePtr&) = delete;//禁用拷貝構造
	UniquePtr& operator=(const UniquePtr&) = delete;//禁用拷貝複製
	UniquePtr(UniquePtr&& object) {//移動構造
		cout << "move construct" << endl;
		ptr_ = object.ptr_;
		object.ptr_ = nullptr;
	}
	UniquePtr& operator=(UniquePtr&& object) {//移動賦值
		cout << "move assign" << endl;
		ptr_ = object.ptr_;
		object.ptr_ = nullptr;
		return *this;
	}
	T*& operator->() { return ptr_; }//->
	T& operator*() { return *ptr_; }//*
    
private:
	T* ptr_;
};

template <typename T>
void ChangeOwnership(UniquePtr<T> move) {
	UniquePtr<T> newOwner{ nullptr };
	newOwner = std::move(move);
}

int main(){
    UniquePtr uniquePtr{new Fruit};
	ChangeOwnership(std::move(uniquePtr));
    //ChangeOwnership(uniquePtr);//compile error! deny copy construction
	//UniquePtr uniquePtr1 = uniquePtr;//compile error! deny copy construction
	//UniquePtr<Fruit> uniquePtr2{nullptr};
	//uniquePtr2 = uniquePtr;//compile error! deny copy assignment
    system("pause");
	return 0;
}

​ 可以看到即使程式設計師沒有自動釋放建立在自由儲存區上的物件,通過UniquePtr也能自動進行釋放。同時UniquePtr無法進行拷貝,保證了UniquePtr對資源所有權的獨佔性,而通過std::move() 以及移動構造/賦值函數,UniquePtr能夠將對資源的所有權轉移給其他UniquePtr物件。基本簡易得實現了一個std::unique_ptr智慧指標。

3 shared_ptr 思想

shared_ptr作為另一個常用的智慧指標,它和unique_ptr智慧指標的理念有著很大的不同,它提供了對資源共用管理,即對資源所有權的共用(shared ownership),這就要求shared_ptr必須是一個可複製的物件。但是由於shared_ptr物件有很多個,而具體的物件資源只有一個這就要求所有共用物件資源的shared_ptrs指標中最終只能有一個shared_ptr能夠釋放物件資源。因此shared_ptr引入了參照計數(reference counting)機制:多個shared_ptrs物件共用一個參照計數變數,通過參照計數記錄當前對物件資源被參照的次數,僅當參照計數為0,也就是出當前shared_ptr物件外沒有其他shared_ptr物件再共用當前物件資源時,當前shared_ptr物件才能夠釋放持有的物件資源。

​ 顯然根據參照計數(reference counting)機制,釋放物件資源的shared_ptr物件必然是最後一個持有物件資源的shared_ptr,這就很好得解決了另一個非常常見的記憶體問題:重複刪除(double deletion)。最後概括一下,shared_ptr要求:

  1. 可複製
  2. 共用參照計數
  3. 自動記憶體管理
  4. 指標操作
template <typename T>
class SharedPtr {
public:
	SharedPtr(T* ptr) :ptr_{ ptr }, count_{ new unsigned int{} } {}
	~SharedPtr() {
		if (*count_ == 0) {//參照計數==0,釋放資源
			cout << "delete shared resource in free store" << endl;
			delete ptr_;
			delete count_;
		}
		else//參照計數不為0,參照計數-1
			--(*count_);
	}
	SharedPtr(const SharedPtr& object) :ptr_{ object.ptr_ }{//拷貝構造 參照+1
		count_ = object.count_;
		++(*count_);
	}
	SharedPtr& operator=(const SharedPtr& object) {//拷貝賦值 參照+1
		ptr_ = object.ptr_;
		count_ = object.count_;
		++(*count_);
		return *this;
	}
	unsigned int GetReferenceCount() { return *count_; }//輸出當前資源參照個數
	T*& operator->() { return ptr_; }//->
	T& operator*() { return *ptr_; }//*

private:
	T* ptr_;
	unsigned int* count_;//reference counting
};

template <typename T>
void ShareOwnership(SharedPtr<T> copy) {
	cout << copy.GetReferenceCount() << endl;
};

int main(){
    SharedPtr sharedPtr1{new Fruit};
	SharedPtr sharedPtr2{ sharedPtr1 };
	SharedPtr<Fruit> sharedPtr3{ nullptr };
	sharedPtr3 = sharedPtr2;
	ShareOwnership(sharedPtr3);
    system("pause");
	return 0;
}

​ 可以看到即使程式中存在多個shared_ptr物件,共用的Fruit物件資源也只會被釋放一次。函數ShareOwnership()中的參照輸出為3,這是因為:首先sharedPtr1持有了一個Fruit物件資源,初始化參照為0;其次sharedPtr2,sharedPtr3通過拷貝sharedPtr1的方式共用了Fruit物件資源,這使得參照0+2=2;最後將sharedPtr3拷貝至函數ShareOwnership()的引數copy中時又使得Fruit物件資源的共用者+1,最終使得參照計數2+1=3;

​ 最後補充一點,對於Fruit物件資源的共用,儘量採用直接拷貝shared_ptr物件的方式進行。如果利用原始Fruit* 指標建立新的shared_ptr物件,則很容易產生 重複刪除(double deletion)問題:

auto sharedPtr{ std::make_shared<Fruit>("apple",2) };
//sharedPtr.get()返回Fruit物件的原始指標Fruit*
std::shared_ptr<Fruit> sharedPtr1{sharedPtr.get() };//cause double deletion

這是因為sharedPtr,sharedPtr1互相不知道對方的存在,都認為只有自己持有Fruit物件,導致兩個shared_ptr的參照計數均為0,當程式走出作用範圍後sharedPtr,sharedPtr1都會嘗試釋放Fruit物件,產生重複刪除(double deletion).