C++學(xué)習(xí)之智能指針中的unique_ptr與shared_ptr
為什么需要智能指針
在上一講《01 C++如何進(jìn)行內(nèi)存資源管理》中,提到了對于堆上的內(nèi)存資源,需要我們手動分配和釋放。管理這些資源是個技術(shù)活,一不小心,就會導(dǎo)致內(nèi)存泄漏。
我們再給兩段代碼,切身體驗(yàn)下原生指針管理內(nèi)存的噩夢。
void foo(int n) { int* ptr = new int(42); ... if (n > 5) { return; } ... delete ptr; } void other_fn(int* ptr) { ... }; void bar() { int* ptr = new int(42); other_fn(ptr); // ptr == ? }
在foo
函數(shù)中,如果入?yún)?code>n > 5, 則會導(dǎo)致指針ptr
的內(nèi)存未被正確釋放,從而導(dǎo)致內(nèi)存泄漏。
在bar
函數(shù)中,我們將指針ptr
傳遞給了另外一個函數(shù)other_fn
,我們無法確定other_fn
有沒有釋放ptr
內(nèi)存,如果被釋放了,那ptr
將成為一個懸空指針,bar
在后續(xù)還繼續(xù)訪問它,會引發(fā)未定義行為,可能導(dǎo)致程序崩潰。
上面由于原生指針使用不當(dāng)導(dǎo)致的內(nèi)存泄漏、懸空指針問題都可以通過智能指針來輕松避免。
C++智能指針是一種用于管理動態(tài)分配內(nèi)存的指針類?;赗AII設(shè)計(jì)理念,通過封裝原生指針實(shí)現(xiàn)的??梢栽谫Y源(原生指針對應(yīng)的對象)生命周期結(jié)束時(shí)自動釋放內(nèi)存。
C++標(biāo)準(zhǔn)庫中,提供了兩種最常見的智能指針類型,分別是std::unique_ptr
和 std::shared_ptr
。
接下來我們分別詳細(xì)展開介紹。
吃獨(dú)食的unique_ptr
std::unique_ptr
是 C++11 引入的智能指針,用于管理動態(tài)分配的內(nèi)存。每個 std::unique_ptr
實(shí)例都擁有對其所包含對象的唯一所有權(quán),并在其生命周期結(jié)束時(shí)自動釋放對象。
創(chuàng)建unique_ptr對象
我們可以std::unique_ptr
的構(gòu)造函數(shù)或std::make_unique
函數(shù)(C++14支持)來創(chuàng)建一個unique_ptr
對象,在超出作用域時(shí),會自動釋放所管理的對象內(nèi)存。示例代碼如下:
#include <memory> #include <iostream> class MyClass { public: MyClass() { std::cout << "MyClass constructed" << std::endl; } ~MyClass() { std::cout << "MyClass destroyed" << std::endl; } }; int main() { std::unique_ptr<MyClass> ptr1(new MyClass); // C++14開始支持std::make_unique std::unique_ptr<int> ptr2 = std::make_unique<int>(10); return 0; }
代碼輸出:
MyClass constructed
MyClass destroyed
訪問所管理的對象
我們可以像使用原生指針的方式一樣,訪問unique_ptr
所指向的對象。也可以通過get
函數(shù)獲取到原生指針。
MyClass* naked_ptr = ptr1.get(); std::cout << *ptr2 << std::endl; // 輸出 10
釋放/重置所管理的對象
使用reset函數(shù)可以釋放unique_ptr所管理的對象,并將其指針重置為nullptr或指定的新指針。reset`大概實(shí)現(xiàn)原理如下
template<class T> void unique_ptr<T>::reset(pointer ptr = pointer()) noexcept { // 釋放指針指向的對象 delete ptr_; // 重置指針 ptr_ = ptr; }
該函數(shù)主要完成兩件事:
- 釋放
std::unique_ptr
所管理的對象,以避免內(nèi)存泄漏。 - 將
std::unique_ptr
重置為nullptr
或管理另一個對象。
code show time:
#include <iostream> #include <memory> class MyClass { public: MyClass() { std::cout << "MyClass constructed" << std::endl; } ~MyClass() { std::cout << "MyClass destroyed" << std::endl; } }; int main() { // 創(chuàng)建一個 std::unique_ptr 對象,指向一個 MyClass 對象 std::unique_ptr<MyClass> ptr(new MyClass); // 調(diào)用 reset,將 std::unique_ptr 重置為管理另一個 MyClass 對象 ptr.reset(new MyClass); return; }
移動所有權(quán)
一個對象資源只能同時(shí)被一個unique_ptr
管理。當(dāng)嘗試把一個unique_ptr
直接賦值給另外一個unique_ptr
會編譯報(bào)錯。
#include <memory> int main() { std::unique_ptr<int> p1 = std::make_unique<int>(42); std::unique_ptr<int> p2 = p1; // 編譯報(bào)錯 return 0; }
為了把一個 std::unique_ptr
對象的所有權(quán)移動到另一個對象中,我們必須配合std::move
移動函數(shù)。
#include <memory> #include <iostream> int main() { std::unique_ptr<int> p1 = std::make_unique<int>(42); std::unique_ptr<int> p2 = std::move(p1); // ok std::cout << *p2 << std::endl; // 42 std::cout << (p1.get() == nullptr) << std::endl; // true return 0; }
這個例子中, 我們把p1
通過std::move
將其管理對象的所有權(quán)轉(zhuǎn)移給了p2
, 此時(shí)p2
接管了對象,而p1
不再擁有管理對象的所有權(quán),即無法再操作到該對象了。
樂于分享的shared_ptr
shared_ptr
是C++11提供的另外一種常見的智能指針,與unique_ptr
獨(dú)占對象方式不同,shared_ptr
是一種共享式智能指針,允許多個shared_ptr
指針共同擁有同一個對象,采用引用計(jì)數(shù)的方式來管理對象的生命周期。當(dāng)所有的 shared_ptr
對象都銷毀時(shí),才會自動釋放所管理的對象。
創(chuàng)建shared_ptr對象
同樣的,C++也提供了std::shared_ptr
構(gòu)造函數(shù)和std::make_shared
函數(shù)來創(chuàng)建std::shared_ptr
對象。
#include <memory> int main() { std::shared_ptr<int> p1(new int(10)); std::shared_ptr<int> p2 = std::make_shared<int>(20); return; }
多個shared_ptr共享一個對象
可以通過賦值操作實(shí)現(xiàn)多個shared_ptr
共享一個資源對象,例如
std::shared_ptr<int>p3 = p2;
shared_ptr
采用引用計(jì)數(shù)的方式管理資源對象的生命周期,通過分配一個額外內(nèi)存當(dāng)計(jì)數(shù)器。
當(dāng)一個新的shared_ptr被創(chuàng)建時(shí),它對應(yīng)的計(jì)數(shù)器被初始化為1。每當(dāng)賦值給另外一個shared_ptr
共享同一個對象時(shí),計(jì)數(shù)器值會加1。當(dāng)某個shared_ptr
被銷毀時(shí),計(jì)數(shù)值會減1,當(dāng)計(jì)數(shù)值變?yōu)?時(shí),說明沒有任何shared_ptr
引用這個對象,會將對象進(jìn)行回收。
C++提供了use_count
函數(shù)來獲取std::shared_ptr
所管理對象的引用計(jì)數(shù),例如
std::cout << "p1 use count: " << p1.use_count() << std::endl;
釋放/重置所管理的對象
可以使用reset
函數(shù)來釋放/重置shared_ptr
所管理的對象。大概實(shí)現(xiàn)原理如下(不考慮并發(fā)場景)
void reset(T* ptr = nullptr) { if (ref_count != nullptr) { (*ref_count)--; if (*ref_count == 0) { delete data; delete ref_count; } } data = ptr; ref_count = (data == nullptr) ? nullptr : new size_t(1); }
data
指針來存儲管理的資源,指針ref_count
來存儲計(jì)數(shù)器的值。
在 reset 方法中,需要減少計(jì)數(shù)器的值,如果計(jì)數(shù)器減少后為 0,則需要釋放管理的資源,如果減少后不為0,則不會釋放之前的資源對象。
如果reset指定了新的資源指針,則需要重新設(shè)置 data 和 ref_count,并將計(jì)數(shù)器初始化為 1。否則,將計(jì)數(shù)器指針置為nullptr
shared_ptr使用注意事項(xiàng)
避免循環(huán)引用
由于 shared_ptr
具有共享同一個資源對象的能力,因此容易出現(xiàn)循環(huán)引用的情況。例如:
struct Node { std::shared_ptr<Node> next; }; int main() { std::shared_ptr<Node> node1(new Node); std::shared_ptr<Node> node2(new Node); node1->next = node2; node2->next = node1; }
在上述代碼中,node1
和 node2
互相引用,在析構(gòu)時(shí)會發(fā)現(xiàn)計(jì)數(shù)器的值不為0,不會釋放所管理的對象,產(chǎn)生內(nèi)存泄漏。
為了避免循環(huán)引用,可以將其中一個指針改為 weak_ptr
類型。weak_ptr
也是一種智能指針,通常配合shared_ptr
一起使用。
weak_ptr是一種弱引用,不對所指向的對象進(jìn)行計(jì)數(shù)引用,也就是說,不增加所指對象的引用計(jì)數(shù)。當(dāng)所有的shared_ptr
都析構(gòu)了,不再指向該資源時(shí),該資源會被銷毀,同時(shí)對應(yīng)的所有weak_ptr
都會變成nullptr
,這時(shí)我們就可以利用expired()
方法來判斷這個weak_ptr
是否已經(jīng)失效。
我們可以通過weak_ptr
的lock()
方法來獲得一個指向共享對象的shared_ptr
。如果weak_ptr
已經(jīng)失效,lock()
方法將返回一個空的shared_ptr
。
下面是weak_ptr
的基本使用示例:
#include <iostream> #include <memory> int main() { std::shared_ptr<int> sp = std::make_shared<int>(42); // 創(chuàng)建shared_ptr對應(yīng)的weak_ptr指針 std::weak_ptr<int> wp(sp); // 通過lock創(chuàng)建一個對應(yīng)的shared_ptr if (auto p = wp.lock()) { std::cout << "shared_ptr value: " << *p << std::endl; std::cout << "shared_ptr use_count: " << p.use_count() << std::endl; } else { std::cout << "wp is expired" << std::endl; } // 釋放shared_ptr指向的資源,此時(shí)weak_ptr失效 sp.reset(); std::cout << "wp is expired: " << wp.expired() << std::endl; return 0; }
代碼輸出如下
shared_ptr value: 42
shared_ptr use_count: 2
wp is expired: 1
回到shared_ptr
的循環(huán)引用問題,利用weak_ptr不會增加shared_ptr的引用計(jì)數(shù)的特點(diǎn),我們將Node.next的類型改為weak_ptr
, 避免node1和node2互相循環(huán)引用。修改后代碼如下
```cpp struct Node { std::weak_ptr<Node> next; }; int main() { std::shared_ptr<Node> node1(new Node); std::shared_ptr<Node> node2(new Node); node1->next = std::weak_ptr<Node>(node2); node2->next = std::weak_ptr<Node>(node1); ; }
避免裸指針與shared_ptr混用
先看看以下代碼
int* q = new int(9); { std::shared_ptr<int> p(new int(10)); ... q = p.get(); } std::cout << *q << std::endl;
get
函數(shù)返回 std::shared_ptr
所持有的指針,但是不會增加引用計(jì)數(shù)。所以在shared_ptr析構(gòu)時(shí),將該指針指向的對象給釋放掉了,導(dǎo)致指針q
變成一個懸空指針。
避免一個原始指針初始化多個shared_ptr
int* p = new int(10); std::shared_ptr<int> ptr1(p); // error: 兩個shared_ptr指向同一個資源,會導(dǎo)致重復(fù)釋放 std::shared_ptr<int> ptr2(p);
總結(jié)
避免手動管理內(nèi)存帶來的繁瑣和容易出錯的問題。我們今天介紹了三種智能指針:unique_ptr
、shared_ptr
和weak_ptr
。
每種智能指針都有各自的使用場景。unique_ptr
用于管理獨(dú)占式所有權(quán)的對象,它不能拷貝但可以移動,是最輕量級和最快的智能指針。shared_ptr
用于管理多個對象共享所有權(quán)的情況,它可以拷貝和移動。weak_ptr
則是用來解決shared_ptr
循環(huán)引用的問題。
到此這篇關(guān)于C++學(xué)習(xí)之智能指針中的unique_ptr與shared_ptr的文章就介紹到這了,更多相關(guān)C++智能指針內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C++實(shí)現(xiàn)LeetCode(13.羅馬數(shù)字轉(zhuǎn)化成整數(shù))
這篇文章主要介紹了C++實(shí)現(xiàn)LeetCode(13.羅馬數(shù)字轉(zhuǎn)化成整數(shù)),本篇文章通過簡要的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下2021-07-07C++開發(fā)的Redis數(shù)據(jù)導(dǎo)入工具優(yōu)化
這篇文章主要介紹了C++開發(fā)的Redis數(shù)據(jù)導(dǎo)入工具優(yōu)化方法的相關(guān)資料,需要的朋友可以參考下2015-07-07C++基礎(chǔ) class、struct、union詳細(xì)
這篇文章主要 給大家介紹的是C++基礎(chǔ) class、struct、union,主要由三部分組成,分別是、類class、結(jié)構(gòu)體struct、共用體union,需要的朋友可以參考一下2021-09-09C++實(shí)現(xiàn)多線程并發(fā)場景下的同步方法
在C++中實(shí)現(xiàn)多線程并發(fā)場景下的同步方法,包括使用互斥鎖、獨(dú)占鎖、共享鎖和原子操作的實(shí)現(xiàn),具有一定的參考價(jià)值,感興趣的可以了解一下2025-02-02C++一個函數(shù)如何調(diào)用其他.cpp文件中的函數(shù)
這篇文章主要介紹了C++一個函數(shù)如何調(diào)用其他.cpp文件中的函數(shù)問題,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-02-02