C++ smart pointer全面深入講解

我們為什麼需要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內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: