為什么需要智能指針
在上一講《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)> 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)存的指針類。基于raii設(shè)計理念,通過封裝原生指針實(shí)現(xiàn)的。可以在資源(原生指針對應(yīng)的對象)生命周期結(jié)束時自動釋放內(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é)束時自動釋放對象。
創(chuàng)建unique_ptr對象
我們可以std::unique_ptr的構(gòu)造函數(shù)或std::make_unique函數(shù)(c++14支持)來創(chuàng)建一個unique_ptr對象,在超出作用域時,會自動釋放所管理的對象內(nèi)存。示例代碼如下:
#include #include class myclass { public: myclass() { std::cout << "myclass constructed" << std::endl; } ~myclass() { std::cout << "myclass destroyed" << std::endl; } }; int main() { std::unique_ptr ptr1(new myclass); // c++14開始支持std::make_unique std::unique_ptr ptr2 = std::make_unique(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 void unique_ptr::reset(pointer ptr = pointer()) noexcept { // 釋放指針指向的對象 delete ptr_; // 重置指針 ptr_ = ptr; }
該函數(shù)主要完成兩件事:
- 釋放std::unique_ptr所管理的對象,以避免內(nèi)存泄漏。
- 將std::unique_ptr重置為nullptr或管理另一個對象。
code show time:
#include #include 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 ptr(new myclass); // 調(diào)用 reset,將 std::unique_ptr 重置為管理另一個 myclass 對象 ptr.reset(new myclass); return; }
移動所有權(quán)
一個對象資源只能同時被一個unique_ptr管理。當(dāng)嘗試把一個unique_ptr直接賦值給另外一個unique_ptr會編譯報錯。
#include int main() { std::unique_ptr p1 = std::make_unique(42); std::unique_ptr p2 = p1; // 編譯報錯 return 0; }
為了把一個std::unique_ptr對象的所有權(quán)移動到另一個對象中,我們必須配合std::move移動函數(shù)。
#include #include int main() { std::unique_ptr p1 = std::make_unique(42); std::unique_ptr 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, 此時p2接管了對象,而p1不再擁有管理對象的所有權(quán),即無法再操作到該對象了。
樂于分享的shared_ptr
shared_ptr是c++11提供的另外一種常見的智能指針,與unique_ptr獨(dú)占對象方式不同,shared_ptr是一種共享式智能指針,允許多個shared_ptr指針共同擁有同一個對象,采用引用計數(shù)的方式來管理對象的生命周期。當(dāng)所有的shared_ptr對象都銷毀時,才會自動釋放所管理的對象。
創(chuàng)建shared_ptr對象
同樣的,c++也提供了std::shared_ptr構(gòu)造函數(shù)和std::make_shared函數(shù)來創(chuàng)建std::shared_ptr對象。
#include int main() { std::shared_ptr p1(new int(10)); std::shared_ptr p2 = std::make_shared(20); return; }
多個shared_ptr共享一個對象
可以通過賦值操作實(shí)現(xiàn)多個shared_ptr共享一個資源對象,例如
std::shared_ptrp3 = p2;
shared_ptr采用引用計數(shù)的方式管理資源對象的生命周期,通過分配一個額外內(nèi)存當(dāng)計數(shù)器。
當(dāng)一個新的shared_ptr被創(chuàng)建時,它對應(yīng)的計數(shù)器被初始化為1。每當(dāng)賦值給另外一個shared_ptr共享同一個對象時,計數(shù)器值會加1。當(dāng)某個shared_ptr被銷毀時,計數(shù)值會減1,當(dāng)計數(shù)值變?yōu)?時,說明沒有任何shared_ptr引用這個對象,會將對象進(jìn)行回收。
c++提供了use_count函數(shù)來獲取std::shared_ptr所管理對象的引用計數(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來存儲計數(shù)器的值。
在 reset 方法中,需要減少計數(shù)器的值,如果計數(shù)器減少后為 0,則需要釋放管理的資源,如果減少后不為0,則不會釋放之前的資源對象。
如果reset指定了新的資源指針,則需要重新設(shè)置 data 和 ref_count,并將計數(shù)器初始化為 1。否則,將計數(shù)器指針置為nullptr
shared_ptr使用注意事項(xiàng)
避免循環(huán)引用
由于shared_ptr具有共享同一個資源對象的能力,因此容易出現(xiàn)循環(huán)引用的情況。例如:
struct node { std::shared_ptr next; }; int main() { std::shared_ptr node1(new node); std::shared_ptr node2(new node); node1->next = node2; node2->next = node1; }
在上述代碼中,node1和node2互相引用,在析構(gòu)時會發(fā)現(xiàn)計數(shù)器的值不為0,不會釋放所管理的對象,產(chǎn)生內(nèi)存泄漏。
為了避免循環(huán)引用,可以將其中一個指針改為weak_ptr類型。weak_ptr也是一種智能指針,通常配合shared_ptr一起使用。
weak_ptr是一種弱引用,不對所指向的對象進(jìn)行計數(shù)引用,也就是說,不增加所指對象的引用計數(shù)。當(dāng)所有的shared_ptr都析構(gòu)了,不再指向該資源時,該資源會被銷毀,同時對應(yīng)的所有weak_ptr都會變成nullptr,這時我們就可以利用expired()方法來判斷這個weak_ptr是否已經(jīng)失效。
我們可以通過weak_ptr的lock()方法來獲得一個指向共享對象的shared_ptr。如果weak_ptr已經(jīng)失效,lock()方法將返回一個空的shared_ptr。
下面是weak_ptr的基本使用示例:
#include #include int main() { std::shared_ptr sp = std::make_shared(42); // 創(chuàng)建shared_ptr對應(yīng)的weak_ptr指針 std::weak_ptr 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指向的資源,此時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的引用計數(shù)的特點(diǎn),我們將node.next的類型改為weak_ptr, 避免node1和node2互相循環(huán)引用。修改后代碼如下
```cpp struct node { std::weak_ptr next; }; int main() { std::shared_ptr node1(new node); std::shared_ptr node2(new node); node1->next = std::weak_ptr(node2); node2->next = std::weak_ptr(node1); ; }
避免裸指針與shared_ptr混用
先看看以下代碼
int* q = new int(9); { std::shared_ptr p(new int(10)); ... q = p.get(); } std::cout << *q << std::endl;
get函數(shù)返回std::shared_ptr所持有的指針,但是不會增加引用計數(shù)。所以在shared_ptr析構(gòu)時,將該指針指向的對象給釋放掉了,導(dǎo)致指針q變成一個懸空指針。
避免一個原始指針初始化多個shared_ptr
int* p = new int(10); std::shared_ptr ptr1(p); // error: 兩個shared_ptr指向同一個資源,會導(dǎo)致重復(fù)釋放 std::shared_ptr 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)引用的問題。