首頁 > 軟體

C++ smart pointer全面深入講解

2022-08-25 10:01:49

我們為什麼需要smart pointer

眾所周知 新手寫的c++程式碼是很恐怖 壓根就不能用 其中最大的原因就在於新手寫的程式碼可能存在大量的記憶體漏失 那麼為什麼新手無法很好的去掌握記憶體的東西呢 就是因為原生的c++並不像java那樣存在垃圾回收的機制 申請在堆區的資源都需要自己去回收 然而最痛苦的一件事情在於 指標的生命週期結束時 你會不小心就沒去回收他在堆區的資源 因為堆區資源的生命週期是很難把握的 有可能你解構了 直接導致野指標存取異常那麼為了解決這個問題 c++就推出了智慧指標 其中最重要的三種指標就是shared_ptr unique_ptr weak_ptr 接下來讓我們來講講如何將智慧指標的生命週期和堆區資源的生命週期繫結起來吧

其實也非常簡單 本質就是當這片堆區資源的參照計數變為0的時候就釋放這片記憶體

smart pointer基本概念之參照計數

先來說說參照計數 這個東西是stl保證了肯定是執行緒安全的 所以即使你在多個執行緒內同時去增加或者同時減少參照計數也並不會讓參照計數的值出現非你預期的結果

智慧指標是和參照計數繫結在一起的 當你建立智慧指標指向一片資源時 參照計數就加一 當智慧指標解構時 參照計數就減一 當參照計數變為0時 堆區資源被解構

smart pointer之shared_ptr

讓我們來看看下一段程式碼

int main()
 {
	std::shared_ptr<std::string> i(new std::string("its good"));
	std::shared_ptr<std::string> j(new std::string("its bad"));
	std::vector<std::shared_ptr<std::string>> smartPointer_vec;
	for(int k=0;k<5;k++)
	smartPointer_vec.emplace_back(i);
	for (int k = 0; k < 4; k++)
	smartPointer_vec.emplace_back(j);
	for (auto &i : smartPointer_vec)
	{
		std::cout<<i->c_str();
		std::cout << i.use_count() << " ";
		std::cout << j.use_count() << std::endl;
		i = nullptr;
	}
	std::cout << i->c_str();
	std::cout << i.use_count() <<" ";
	std::cout << j.use_count() << std::endl;
}

聰明人看輸出 你就能完全明白 當參照計數為0的時候就會解構 其他不多說了

重要講解:首先使用share_ptr去指向new出來的資料是效能低效的 最本質的原因在於 他會進行兩次記憶體分配 第一次是物件堆區資源的申請 然後才是參照計數堆區資源的申請 而使用make_shared可以只進行一次記憶體分配 所以他更快 並且更安全 並且c++標準委員會也推薦你這麼做 關於make_shared等下講解

自定義deleter(也就是自定義刪除器)

先說我們為什麼需要自定義刪除器 因為在某些情況下 我們希望當智慧指標指向的堆區資源釋放的時候進行一些自定義操作也就是說你可以玩一些很花的操作 但是也是那句話 stl並不會執行任何安全檢查 崩了需要自己負責並且總所周知 new []這種形式的堆區資源需要我們使用delete[]來釋放 這就是最大的問題 shared_ptr預設是使用delete的 也就是說 當你使用shared_ptr去指向new []時如果不自定義刪除器 必然會造成記憶體漏失 如下圖所示的一段程式碼就是經典的記憶體漏失

正確的寫法如下

即自定義一個刪除器 當然你也可以玩一些移動操作 也就是花哨的操作 當然花哨操作就很多了 我只演示其中一種如下圖所示

執行結果截圖如下:

Tips:當你非常清楚你在幹什麼的時候再玩 功力不夠 不要亂玩

shared_ptr之make_shared

上文我們說過 使用智慧指標指向new出來的資源有一個問題就是他會進行兩次記憶體分配 而標準委員會推薦建立shared_ptr的方式是使用make_shared 讓我們來看看make_shared是如何進行堆區資源申請的 一個最簡單的例子如下

int main()
{
	std::shared_ptr<int>p1(new int(5));
	//下面這種方式比上面這種方式效能更快 並且更加安全
	std::shared_ptr<int>p2 = make_shared<int>(5);
}

當你使用make_shared的時候 又想去使用智慧指標指向一個陣列的時候 一個推薦的做法如下

int main()
{
	std::shared_ptr<std::vector<int>>p1(new std::vector<int>());
	//下面這種方式比上面這種方式效能更快 並且更加安全
	std::shared_ptr<std::vector<int>>p2 = make_shared<std::vector<int>>();
}

智慧指標存在的問題之迴圈參照

那麼現在我們來看看shared_ptr存在的一些問題 其中比較著名的一個問題就是迴圈參照 什麼叫回圈參照呢 本人的觀點是當你的智慧指標指向的A堆區資源裡又有智慧指標去指向B堆區資源 而B堆區資源又存在一個智慧指標來指向A堆區資源 而你能拿到的指標對半是全域性或者是棧區的智慧指標 你無法干預到堆區的智慧指標的釋放 下面來看一個最簡單的例子造成的迴圈參照 程式碼如下圖所示

class SmartPointerTest
{
public:
	std::shared_ptr<SmartPointerTest> LoopRef{};
	int p[1000]{};
};
int main()
{
	std::shared_ptr<SmartPointerTest>p1(new SmartPointerTest());
	std::shared_ptr<SmartPointerTest>p2(new SmartPointerTest());
	p1->LoopRef = p2;
	p2->LoopRef = p1;
}

可以明顯看到 我們建立了兩個智慧指標p1和p2 而p1指向的堆區資源裡又有智慧指標指向p2的堆區資源 同理p2 而當main函數結束的時候 p1 p2指標被釋放 但是 這個時候 因為兩片堆區資源的參照計數都沒被置為0 所以不會釋放 那麼這片堆區記憶體也就永遠的洩漏了 這是所有迴圈參照的原型 無論任何再複雜的迴圈參照都是建立在這個最基本的迴圈參照之上的

解決迴圈參照之weak_ptr

我們現在希望有一個方法來解決迴圈參照的問題 並且我們也想去隨時拿到資源 那麼我們該如何做呢 標準委員會也考慮到了這個問題 於是他提供了weak_ptr 當他指向一片堆區資源的時候 並不會讓這片堆區資源的參照計數加一 而是作為這片資源的觀察者 當需要這片資源的時候 隨時使用lock()函數來獲得一個shared_ptr來進行使用 下面讓我們來看看如何使用weak_ptr 基於上面的例子

class SmartPointerTest
{
public:
	std::weak_ptr<SmartPointerTest> LoopRef{};
	int p[1000]{};
};
int main()
{
	std::shared_ptr<SmartPointerTest>p1(new SmartPointerTest());
	std::shared_ptr<SmartPointerTest>p2(new SmartPointerTest());
	p1->LoopRef = p2;
	p2->LoopRef = p1;
	//當你想使用資源的時候 用下面的操作進行
	std::cout << p1->LoopRef.lock()->p << std::endl;
}

輸出結果如下:

Tips:當然weak_ptr的作用遠遠不止如此 他存在的意義僅僅是你想共用資源但是你並不想增加參照計數 解決迴圈參照只是順便解決的優秀的程式設計師總是能知道在什麼情況下使用何種指標來達到效能最優 lock()函數 顧名思義是要去給參照計數上鎖的 頻繁上鎖帶來的效能問題不用多說了吧

如果weak_ptr指向的資源已經被解構 那麼他會丟擲bad_weak_ptr的異常 請注意捕獲異常

智慧指標問題

無法建立指向自己的智慧指標(本質當建立自己的智慧指標時會建立兩個所屬組)

什麼叫無法建立指向自己的智慧指標呢 看如下這段程式碼

class SmartPointerTest
{
public:
	std::weak_ptr<SmartPointerTest> LoopRef{};
	int p[1000]{};
	std::vector<std::shared_ptr<SmartPointerTest>> spt_vec;
	void MemberFuncTest()
	{
		spt_vec.push_back(std::shared_ptr<SmartPointerTest>(this));
	}
	int operator[](int i)
	{
		return p[i];
	}
};
int main()
{
	std::shared_ptr<SmartPointerTest>p1(new SmartPointerTest());
	p1->MemberFuncTest();
	std::cout<<p1.use_count()<<std::endl;
	system("pause");
}

我們預期的結果是把指向自己的智慧指標傳入 並且參照計數為2 但是執行結果如下:

並且程式會崩潰 為什麼呢 因為你重複釋放了 這就是我說的 你會建立兩個組 而不是單純的增加參照計數 其本質還是濫用普通指標和智慧指標引起的麻煩

解決方法如下

程式碼如下 我們可以繼承於std::enable_shared_from_this來解決

class SmartPointerTest :std::enable_shared_from_this<SmartPointerTest>
{
public:
	std::weak_ptr<SmartPointerTest> LoopRef{};
	int p[1000]{};
	std::vector<std::shared_ptr<SmartPointerTest>> spt_vec;
	void MemberFuncTest()
	{
		spt_vec.push_back(std::shared_ptr<SmartPointerTest>(shared_from_this()));
	}
	int operator[](int i)
	{
		return p[i];
	}
};
int main()
{
	std::shared_ptr<SmartPointerTest>p1(new SmartPointerTest());
	p1->MemberFuncTest();
	std::cout<<p1.use_count()<<std::endl;
	system("pause");
}

當你這樣繼承自enable_shared_from_this的時候你就可以將自身的智慧指標傳入而不是建立一個新的組避免了重複釋放非常的方便

關於unique_ptr我們將會在下一篇文章進行詳細講解其實也很簡單就是他堆區資源的參照計數永遠只可能是一也就是說他的資源只可能被一個指標指向附帶而來的有一些小細節和普通的shared_ptr不同我們也就留在下一章再說了

到此這篇關於C++ smart pointer全面深入講解的文章就介紹到這了,更多相關C++ smart pointer內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com