智能指針-使用、避坑和實(shí)現(xiàn)

大家好,今天借助本文,從實(shí)踐、避坑和實(shí)現(xiàn)原理三個角度分析下C++中的智能指針。
本文主要內(nèi)容如下圖所示:

智能指針的由來 auto_ptr為什么被廢棄 unique_ptr的使用、特點(diǎn)以及實(shí)現(xiàn) shared_ptr的使用、特點(diǎn)以及實(shí)現(xiàn) weak_ptr的使用、特點(diǎn)以及實(shí)現(xiàn) 介紹筆者在工作中遇到的一些職能指針相關(guān)的坑,并給出一些建議
背景
內(nèi)存的分配與回收都是由開發(fā)人員在編寫代碼時主動完成的,好處是內(nèi)存管理的開銷較小,程序擁有更高的執(zhí)行效率;弊端是依賴于開發(fā)者的水平,隨著代碼規(guī)模的擴(kuò)大,極容易遺漏釋放內(nèi)存的步驟,或者一些不規(guī)范的編程可能會使程序具有安全隱患。如果對內(nèi)存管理不當(dāng),可能導(dǎo)致程序中存在內(nèi)存缺陷,甚至?xí)谶\(yùn)行時產(chǎn)生內(nèi)存故障錯誤。換句話說,開發(fā)者自己管理內(nèi)存,最容易發(fā)生下面兩種情況:
申請了內(nèi)存卻沒有釋放,造成內(nèi)存泄漏 使用已經(jīng)釋放的內(nèi)存,造成 segment fault
所以,為了在保證性能的前提下,又能使得開發(fā)者不需要關(guān)心內(nèi)存的釋放,進(jìn)而使得開發(fā)者能夠?qū)⒏嗟木ν度氲綐I(yè)務(wù)上,自C++11開始,STL正式引入了智能指針。
所有權(quán)
智能指針一個很關(guān)鍵的一個點(diǎn)就是是否擁有一個對象的所有權(quán),當(dāng)我們通過std::make_xxx或者new一個對象,那么就擁有了這個對象的所有權(quán)。
所有權(quán)分為獨(dú)占所有權(quán)、共享所有權(quán)以及弱共享所有權(quán)三種。
獨(dú)占所有權(quán)
顧名思義,獨(dú)占該對象。獨(dú)占的意思就是不共享,所有權(quán)可以轉(zhuǎn)移,但是轉(zhuǎn)移之后,所有權(quán)也是獨(dú)占。auto_ptr和unique_ptr就是一種獨(dú)占所有權(quán)方式的智能指針。
假設(shè)有個Object對象,如果A擁有該對象的話,就需要保證其在不使用該對象的時候,將該對象釋放;而此時如果B也想擁有Object對象,那么就必須先讓A放棄該對象所有權(quán),然后B獨(dú)享該對象,那么該對象的使用和釋放就只歸B所有,跟A沒有關(guān)系了。
獨(dú)占所有權(quán)具有以下幾個特點(diǎn):
如果創(chuàng)建或者復(fù)制了某個對象,就擁有了該對象 如果沒有創(chuàng)建對象,而是將對象保留使用,同樣擁有該對象的所有權(quán) 如果你擁有了某個對象的所有權(quán),在不需要某一個對象時,需要釋放它們
共享所有權(quán)
共享所有權(quán),與獨(dú)占所有權(quán)正好相反,對某個對象的所有權(quán)可以共享。shared_ptr就是一種共享所有權(quán)方式的智能指針。
假設(shè)此時A擁有對象Object,在沒有其它擁有該對對象的情況下,對象的釋放由A來負(fù)責(zé);如果此時B也想擁有該對象,那么對象的釋放由最后一個擁有它的來負(fù)責(zé)。
舉一個我們經(jīng)常遇到的例子,socket連接,多個發(fā)送端(sender)可以使用其發(fā)送和接收數(shù)據(jù)。

弱共享所有權(quán)
弱共享所有權(quán),指的是可以使用該對象,但是沒有所有權(quán),由真正擁有其所有權(quán)的來負(fù)責(zé)釋放。weak_ptr就是一種弱共享所有權(quán)方式的智能指針。

分類
在C++11中,有unique_ptr、shared_ptr以及weak_ptr三種,auto_ptr因?yàn)樽陨?code style="overflow-wrap: break-word;margin-right: 2px;margin-left: 2px;font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(53, 148, 247);background: rgba(59, 170, 250, 0.1);padding-right: 2px;padding-left: 2px;border-radius: 2px;height: 21px;line-height: 22px;">轉(zhuǎn)移所有權(quán)的原因,在C++11中被廢棄(本節(jié)最后,將簡單說下被廢棄的原因)。
unique_ptr
使用上限制最多的一種智能指針,被用來取代之前的auto_ptr,一個對象只能被一個unique_ptr所擁有,而不能被共享,如果需要將其所擁有的對象轉(zhuǎn)移給其他unique_ptr,則需要使用move語義 shared_ptr
與unique_ptr不同的是,unique_ptr是 獨(dú)占管理權(quán),而shared_ptr則是共享管理權(quán),即多個shared_ptr可以共用同一塊關(guān)聯(lián)對象,其內(nèi)部采用的是引用計(jì)數(shù),在拷貝的時候,引用計(jì)數(shù)+1,而在某個對象退出作用域或者釋放的時候,引用計(jì)數(shù)-1,當(dāng)引用計(jì)數(shù)為0的時候,會自動釋放其管理的對象。weak_ptr
weak_ptr的出現(xiàn),主要是為了解決shared_ptr的 循環(huán)引用,其主要是與shared_ptr一起來使用。和shared_ptr不同的地方在于,其并不會擁有資源,也就是說不能訪問對象所提供的成員函數(shù),不過,可以通過weak_ptr.lock()來產(chǎn)生一個擁有訪問權(quán)限的shared_ptr。
auto_ptr
auto_ptr自C++98被引入,因?yàn)槠浯嬖谳^多問題,所以在c++11中被廢棄,自C++17開始正式從STL中移除。
首先我們看下auto_ptr的簡單實(shí)現(xiàn)(為了方便閱讀,進(jìn)行了修改,基本功能類似于std::auto_ptr):
template<class T>
class auto_ptr
{
T* p;
public:
auto_ptr(T* s) :p(s) {}
~auto_ptr() { delete p; }
auto_ptr(auto_ptr& a) {
p = a.p;
a.p = NULL;
}
auto_ptr& operator=(auto_ptr& a) {
delete p;
p=a.p;
a.p = NULL;
return *this;
}
T& operator*() const { return *p; }
T* operator->() const { return p; }
};
從上面代碼可以看出,auto_ptr采用copy語義來轉(zhuǎn)移所有權(quán),轉(zhuǎn)移之后,其關(guān)聯(lián)的資源指針設(shè)置為NULL,而這跟我們理解上copy行為不一致。
在<< Effective STL >>第8條,作者提出永不建立auto_ptr的容器,并以一個例子來說明原因,感興趣的可以去看看這本書,還是不錯的。
實(shí)際上,auto_ptr被廢棄的直接原因是拷貝造成所有權(quán)轉(zhuǎn)移,如下代碼:
auto_ptr<ClassA> a(new ClassA);
auto_ptr<ClassA> b = a;
a->Method();
在上述代碼中,因?yàn)?code style="overflow-wrap: break-word;margin-right: 2px;margin-left: 2px;font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(53, 148, 247);background: rgba(59, 170, 250, 0.1);padding-right: 2px;padding-left: 2px;border-radius: 2px;height: 21px;line-height: 22px;">b = a導(dǎo)致所有權(quán)被轉(zhuǎn)移,即a關(guān)聯(lián)的對象為NULL,如果再調(diào)用a的成員函數(shù),顯然會造成coredump。
正是因?yàn)?code style="overflow-wrap: break-word;margin-right: 2px;margin-left: 2px;font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(53, 148, 247);background: rgba(59, 170, 250, 0.1);padding-right: 2px;padding-left: 2px;border-radius: 2px;height: 21px;line-height: 22px;">拷貝導(dǎo)致所有權(quán)被轉(zhuǎn)移,所以auto_ptr使用上有很多限制:
不能在STL容器中使用,因?yàn)閺?fù)制將導(dǎo)致數(shù)據(jù)無效 一些STL算法也可能導(dǎo)致auto_ptr失效,比如std::sort算法 不能作為函數(shù)參數(shù),因?yàn)檫@會導(dǎo)致復(fù)制,并且在調(diào)用后,導(dǎo)致原數(shù)據(jù)無效 如果作為類的成員變量,需要注意在類拷貝時候?qū)е碌臄?shù)據(jù)無效
正是因?yàn)閍uto_ptr的諸多限制,所以自C++11起,廢棄了auto_ptr,引入unique_ptr。
unique_ptr
unique_ptr是C++11提供的用于防止內(nèi)存泄漏的智能指針中的一種實(shí)現(xiàn)(用來替代auto_ptr),獨(dú)享被管理對象指針?biāo)袡?quán)的智能指針。
unique_ptr對象包裝一個原始指針,并負(fù)責(zé)其生命周期。當(dāng)該對象被銷毀時,會在其析構(gòu)函數(shù)中刪除關(guān)聯(lián)的原始指針。具有->和*運(yùn)算符重載符,因此它可以像普通指針一樣使用。
分類
unique_ptr分為以下兩種:
指向單個對象
std::unique_ptr<Type> p1; // p1關(guān)聯(lián)Type對象
指向一個數(shù)組
unique_ptr<Type[]> p2; // p2關(guān)聯(lián)Type對象數(shù)組
特點(diǎn)
在前面的內(nèi)容中,我們已經(jīng)提到了unique_ptr的特點(diǎn),主要具有以下:
獨(dú)享所有權(quán),在作用域結(jié)束時候,自動釋放所關(guān)聯(lián)的對象
void fun() {
unique_ptr<int> a(new int(1));
}
無法進(jìn)行拷貝與賦值操作
unique_ptr<int> ptr(new int(1));
unique_ptr<int> ptr1(ptr) ; // error
unique_ptr<int> ptr2 = ptr; //error
顯示的所有權(quán)轉(zhuǎn)移(通過move語義)
unique_ptr<int> ptr(new int(1));
unique_ptr<int> ptr1 = std::move(ptr) ; // ok
作為容器元素存儲在容器中
unique_ptr<int> ptr(new int(1));
std::vector<unique_ptr<int>> v;
v.push_back(ptr); // error
v.push_back(std::move(ptr)); // ok
std::cout << *ptr << std::endl;// error
需要注意的是,自c++14起,可以使用下面的方式對unique_ptr進(jìn)行初始化:
auto p1 = std::make_unique<double>(3.14);
auto p2 = std::make_unique<double[]>(n);
如果在c++11中使用上述方法進(jìn)行初始化,會得到下面的錯誤提示:
error: ‘make_unique’ is not a member of ‘std’
因此,如果為了使得c++11也可以使用std::make_unique,我們可以自己進(jìn)行封裝,如下:
namespace details {
#if __cplusplus >= 201402L // C++14及以后使用STL實(shí)現(xiàn)的
using std::make_unique;
#else
template<typename T, typename... Args>
std::unique_ptr<T> make_unique(Args &&... args)
{
return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}
#endif
} // namespace details
使用
為了盡可能了解unique_ptr的使用姿勢,我們使用以下代碼為例:
#include <memory>
#include <utility> // std::move
void fun1(double *);
void fun2(std::unique<double> *);
void fun3(std::unique<double> &);
void fun4(std::unique<double> );
int main() {
std::unique_ptr<double> p(new double(3.14));
fun1(p.get());
fun2(&p);
fun3(p);
if (p) {
std::cout << "is valid" << std::endl;
}
auto p2(p.release()); // 轉(zhuǎn)移所有權(quán)
auto p2.reset(new double(1.0));
fun4(std::move(p2));
return 0;
}
上述代碼,基本覆蓋了常見的unique_ptr用法:
第10行,通過new 創(chuàng)建一個unique_ptr對象第11行,通過get()函數(shù)獲取其關(guān)聯(lián)的 原生指針第12行,通過unique_ptr對象的 指針進(jìn)行訪問第13行,通過unique_ptr對象的 引用進(jìn)行訪問第16行,通過if(p)來判斷其是否 有效第18行,通過release函數(shù) 釋放所有權(quán),并將所有權(quán)進(jìn)行轉(zhuǎn)移第19行,通過reset 釋放之前的原生指針,并重新關(guān)聯(lián)一個新的指針第20行,通過std::move 轉(zhuǎn)移所有權(quán)
簡單實(shí)現(xiàn)
本部分只是基于源碼的一些思路,便于理解,實(shí)現(xiàn)的一個簡單方案,如果想要閱讀源碼,請點(diǎn)擊unique_ptr查看。
基本代碼如下:
template<class T>
class unique_ptr
{
T* p;
public:
unique_ptr() :p() {}
unique_ptr(T* s) :p(s) {}
~unique_ptr() { delete p; }
unique_ptr(const unique_ptr&) = delete;
unique_ptr& operator=(const unique_ptr&) = delete;
unique_ptr(unique_ptr&& s) :p(s.p) { s.p = nullptr }
unique_ptr& operator=(unique_ptr s)
{ delete p; p = s.p; s.p=nullptr; return *this; }
T* operator->() const { return p; }
T& operator*() const { return *p; }
};
從上面代碼基本可以看出,unique_ptr的控制權(quán)轉(zhuǎn)移是通過move語義來實(shí)現(xiàn)的,相比于auto_ptr的拷貝語義轉(zhuǎn)移所有權(quán),更為合理。
shared_ptr
unique_ptr因?yàn)槠渚窒扌?獨(dú)享所有權(quán)),一般很少用于多線程操作。在多線程操作的時候,既可以共享資源,又可以自動釋放資源,這就引入了shared_ptr。
shared_ptr為了支持跨線程訪問,其內(nèi)部有一個引用計(jì)數(shù)(線程安全),用來記錄當(dāng)前使用該資源的shared_ptr個數(shù),在結(jié)束使用的時候,引用計(jì)數(shù)為-1,當(dāng)引用計(jì)數(shù)為0時,會自動釋放其關(guān)聯(lián)的資源。
特點(diǎn)
相對于unique_ptr的獨(dú)享所有權(quán),shared_ptr可以共享所有權(quán)。其內(nèi)部有一個引用計(jì)數(shù),用來記錄共享該資源的shared_ptr個數(shù),當(dāng)共享數(shù)為0的時候,會自動釋放其關(guān)聯(lián)的資源。
shared_ptr不支持?jǐn)?shù)組,所以,如果用shared_ptr指向一個數(shù)組的話,需要自己手動實(shí)現(xiàn)deleter,如下所示:
std::shared_ptr<int> p(new int[8], [](int *ptr){delete []ptr;});
使用
仍然以一段代碼來說明,畢竟代碼更有說服力。
#include <iostream>
#include <memory>
int main() {
// 創(chuàng)建shared_ptr對象
std::shared_ptr<int> p1 = std::make_shared<int>();
*p1 = 78;
std::cout << "p1 = " << *p1 << std::endl;
// 打印引用計(jì)數(shù)
std::cout << "p1 Reference count = " << p1.use_count() << std::endl;
std::shared_ptr<int> p2(p1);
// 打印引用計(jì)數(shù)
std::cout << "p2 Reference count = " << p2.use_count() << std::endl;
std::cout << "p1 Reference count = " << p1.use_count() << std::endl;
if (p1 == p2)
{
std::cout << "p1 and p2 are pointing to same pointer\n";
}
std::cout<<"Reset p1 "<<std::endl;
// 引用計(jì)數(shù)-1
p1.reset();
std::cout << "p1 Reference Count = " << p1.use_count() << std::endl;
// 重置
p1.reset(new int(11));
std::cout << "p1 Reference Count = " << p1.use_count() << std::endl;
p1 = nullptr;
std::cout << "p1 Reference Count = " << p1.use_count() << std::endl;
if (!p1) // 通過此種方式來判斷關(guān)聯(lián)的資源是否為空
{
std::cout << "p1 is NULL" << std::endl;
}
return 0;
}
輸出如下:
p1 = 78
p1 Reference count = 1
p2 Reference count = 2
p1 Reference count = 2
p1 and p2 are pointing to same pointer
Reset p1
p1 Reference Count = 0
p1 Reference Count = 1
p1 Reference Count = 0
p1 is NULL
上面代碼基本羅列了shared_ptr的常用方法,對于其他方法,可以參考源碼或者官網(wǎng)。
線程安全
可能很多人都對shared_ptr是否線程安全存在疑惑,借助本節(jié),對線程安全方面的問題進(jìn)行分析和解釋。
shared_ptr的線程安全問題主要有以下兩種:
引用計(jì)數(shù)的加減操作是否線程安全shared_ptr修改指向時是否線程安全
引用計(jì)數(shù)
shared_ptr中有兩個指針,一個指向所管理數(shù)據(jù)的地址,另一個一個指向執(zhí)行控制塊的地址。
執(zhí)行控制塊包括對關(guān)聯(lián)資源的引用計(jì)數(shù)以及弱引用計(jì)數(shù)等。在前面我們提到shared_ptr支持跨線程操作,引用計(jì)數(shù)變量是存儲在堆上的,那么在多線程的情況下,指向同一數(shù)據(jù)的多個shared_ptr在進(jìn)行計(jì)數(shù)的++或--時是否線程安全呢?
引用計(jì)數(shù)在STL中的定義如下:
_Atomic_word _M_use_count; // #shared
_Atomic_word _M_weak_count; // #weak + (#shared != 0)
當(dāng)對shared_ptr進(jìn)行拷貝時,引入計(jì)數(shù)增加,實(shí)現(xiàn)如下:
template<>
inline void
_Sp_counted_base<_S_atomic>::
_M_add_ref_lock() {
// Perform lock-free add-if-not-zero operation.
_Atomic_word __count;
do
{
__count = _M_use_count;
if (__count == 0)
__throw_bad_weak_ptr();
}
while (!__sync_bool_compare_and_swap(&_M_use_count, __count,
__count + 1));
}
最終,計(jì)數(shù)的增加,是調(diào)用__sync_bool_compare_and_swap實(shí)現(xiàn)的,而該函數(shù)是線程安全的,因此我們可以得出結(jié)論:在多線程環(huán)境下,管理同一個數(shù)據(jù)的shared_ptr在進(jìn)行計(jì)數(shù)的增加或減少的時候是線程安全的,這是一波原子操作。
修改指向
修改指向分為操作同一個對象和操作不同對象兩種。
同一對象
以下面代碼為例:
void fun(shared_ptr<Type> &p) {
if (...) {
p = p1;
} else {
p = p2;
}
}
當(dāng)在多線程場景下調(diào)用該函數(shù)時候,p之前的引用計(jì)數(shù)要進(jìn)行-1操作,而p1對象的引用計(jì)數(shù)要進(jìn)行+1操作,雖然這倆的引用計(jì)數(shù)操作都是線程安全的,但是對這倆對象的引用計(jì)數(shù)的操作在一起時候卻不是線程安全的。這是因?yàn)楫?dāng)對p1的引用計(jì)數(shù)進(jìn)行+1時候,恰恰前一時刻,p1的對象被釋放,后面再進(jìn)行+1操作,會導(dǎo)致segment fault。
不同對象
代碼如下:
void fun1(std::shared_ptr<Type> &p) {
p = p1;
}
void fun2(std::shared_ptr<Type> &p) {
p = p2;
}
int main() {
std::shared_ptr<Type> p = std::make_shared<Type>();
auto p1 = p;
auto p2 = p;
std::thread t1(fun1, p1);
std::thread t2(fun2, p2);
t1.join();
t2.join();
return 0;
}
在上述代碼中,p、p1、p2指向同一個資源,分別有兩個線程操作不同的shared_ptr對象(雖然關(guān)聯(lián)的底層資源是同一個),這樣在多線程下,只對p1和p2的引用計(jì)數(shù)進(jìn)行操作,不會引起segment fault,所以是線程安全的。
?同一個shared_ptr被多個線程
同時讀是安全的同一個shared_ptr被多個線程同時
?讀寫是不安全的
簡單實(shí)現(xiàn)
本部分只是基于源碼的一些思路,便于理解,實(shí)現(xiàn)的一個簡單方案,如果想要閱讀源碼,請點(diǎn)擊shared_ptr查看。
記得之前看過一個問題為什么引用計(jì)數(shù)要new,這個問題我在面試的時候也問過,很少有人能夠回答出來,其實(shí),很簡單,因?yàn)?code style="overflow-wrap: break-word;margin-right: 2px;margin-left: 2px;font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(53, 148, 247);background: rgba(59, 170, 250, 0.1);padding-right: 2px;padding-left: 2px;border-radius: 2px;height: 21px;line-height: 22px;">要支持多線程訪問,所以只能要new呀??。
代碼如下:
template <class T>
class weak_ptr;
class Counter {
public:
Counter() = default;
int s_ = 0; // shared_ptr的計(jì)數(shù)
int w_ = 0; // weak_ptr的計(jì)數(shù)
};
template <class T>
class shared_ptr {
public:
shared_ptr(T *p = 0) : ptr_(p) {
cnt_ = new Counter();
if (p) {
cnt_->s_ = 1;
}
}
~shared_ptr() {
release();
}
shared_ptr(shared_ptr<T> const &s) {
ptr_ = s.ptr_;
(s.cnt)->s_++;
cnt_ = s.cnt_;
}
shared_ptr(weakptr_<T> const &w) {
ptr_ = w.ptr_;
(w.cnt_)->s_++;
cnt_ = w.cnt_;
}
shared_ptr<T> &operator=(shared_ptr<T> &s) {
if (this != &s) {
release();
(s.cnt_)->s_++;
cnt_ = s.cnt_;
ptr_ = s.ptr_;
}
return *this;
}
T &operator*() {
return *ptr_;
}
T *operator->() {
return ptr_;
}
friend class weak_ptr<T>;
protected:
void release() {
cnt_->s_--;
if (cnt_->s_ < 1)
{
delete ptr_;
if (cnt_->w_ < 1)
{
delete cnt_;
cnt_ = NULL;
}
}
}
private:
T *ptr_;
Counter *cnt_;
};
weak_ptr
在三個智能指針中,weak_ptr是存在感最低的一個,也是最容易被大家忽略的一個智能指針。它的引入是為了解決shared_ptr存在的一個問題循環(huán)引用。
特點(diǎn)
不具有普通指針的行為,沒有重載operator*和operator-> 沒有共享資源,它的構(gòu)造不會引起引用計(jì)數(shù)增加 用于協(xié)助shared_ptr來解決循環(huán)引用問題 可以從一個shared_ptr或者另外一個weak_ptr對象構(gòu)造,進(jìn)而可以間接獲取資源的弱共享權(quán)。
使用
int main() {
std::shared_ptr<int> p1 = std::make_shared<Entity>(14);
{
std::weak_ptr<int> weak = p1;
std::shared_ptr<Entity> new_shared = weak.lock();
shared_e1 = nullptr;
new_shared = nullptr;
if (weak.expired()) {
std::cout << "weak pointer is expired" << std::endl;
}
new_shared = weak.lock();
std::cout << new_shared << std::endl;
}
return 0;
}
上述代碼輸出如下:
weak pointer is expired
0
使用成員函數(shù)use_count()和expired()來獲取資源的引用計(jì)數(shù),如果返回為0或者false,則表示關(guān)聯(lián)的資源不存在 使用lock()成員函數(shù)獲得一個可用的shared_ptr對象,進(jìn)而操作資源 當(dāng)expired()為true的時候,lock()函數(shù)將返回一個空的shared_ptr
簡單實(shí)現(xiàn)
template <class T>
class weak_ptr
{
public:
weak_ptr() = default;
weak_ptr(shared_ptr<T> &s) : ptr_(s.ptr_), cnt(s.cnt_) {
cnt_->w_++;
}
weak_ptr(weak_ptr<T> &w) : ptr_(w.ptr_), cnt_(w.cnt_) {
cnt_->w_++;
}
~weak_ptr() {
release();
}
weak_ptr<T> &operator=(weak_ptr<T> &w) {
if (this != &w) {
release();
cnt_ = w.cnt_;
cnt_->w_++;
ptr_ = w.ptr_;
}
return *this;
}
weak_ptr<T> &operator=(shared_ptr<T> &s)
{
release();
cnt_ = s.cnt_;
cnt_->w_++;
ptr_ = s.ptr_;
return *this;
}
shared_ptr<T> lock() {
return shared_ptr<T>(*this);
}
bool expired() {
if (cnt) {
if (cnt->s_ > 0) {
return false;
}
}
return true;
}
friend class shared_ptr<T>;
protected:
void release() {
if (cnt_) {
cnt_->w_--;
if (cnt_->w_ < 1 && cnt_->s_ < 1) {
cnt_ = nullptr;
}
}
}
private:
T *ptr_ = nullptr;
Counter *cnt_ = nullptr;
};
循環(huán)引用
在之前的文章內(nèi)存泄漏-原因、避免以及定位中,我們講到使用weak_ptr來配合shared_ptr使用來解決循環(huán)引用的問題,借助本文,我們深入說明下如何來解決循環(huán)引用的問題。
代碼如下:
class Controller {
public:
Controller() = default;
~Controller() {
std::cout << "in ~Controller" << std::endl;
}
class SubController {
public:
SubController() = default;
~SubController() {
std::cout << "in ~SubController" << std::endl;
}
std::shared_ptr<Controller> controller_;
};
std::shared_ptr<SubController> sub_controller_;
};
在上述代碼中,因?yàn)閏ontroller和sub_controller之間都有一個指向?qū)Ψ降膕hared_ptr,這樣就導(dǎo)致任意一個都因?yàn)閷Ψ接幸粋€指向自己的對象,進(jìn)而引用計(jì)數(shù)不能為0。

為了解決std::shared_ptr循環(huán)引用導(dǎo)致的內(nèi)存泄漏,我們可以使用std::weak_ptr來單面去除上圖中的循環(huán)。
class Controller {
public:
Controller() = default;
~Controller() {
std::cout << "in ~Controller" << std::endl;
}
class SubController {
public:
SubController() = default;
~SubController() {
std::cout << "in ~SubController" << std::endl;
}
std::weak_ptr<Controller> controller_;
};
std::shared_ptr<SubController> sub_controller_;
};
在上述代碼中,我們將SubController類中controller_的類型從std::shared_ptr變成std::weak_ptr。

那么,為什么將SubController中的shared_ptr換成weak_ptr就能解決這個問題呢?我們看下源碼:
template<typename _Tp1>
__weak_ptr&
operator=(const __shared_ptr<_Tp1, _Lp>& __r) // never throws
{
_M_ptr = __r._M_ptr;
_M_refcount = __r._M_refcount;
return *this;
}
在上面代碼中,我們可以看到,將一個shared_ptr賦值給weak_ptr的時候,其引用計(jì)數(shù)并沒有+1,所以也就解決了循環(huán)引用的問題。
那么,如果我們想要使用shared_ptr關(guān)聯(lián)的對象進(jìn)行操作時候,該怎么做呢?使用weak_ptr::lock()函數(shù)來實(shí)現(xiàn),源碼如下:
__shared_ptr<_Tp, _Lp>
lock() const {
return expired() ? __shared_ptr<element_type, _Lp>() : __shared_ptr<element_type, _Lp>(*this);
}
從上面代碼可看出,使用lock()函數(shù)生成一個shared_ptr供使用,如果之前的shared_ptr已經(jīng)被釋放,那么就返回一個空shared_ptr對象,否則生成shared_ptr對象的拷貝(這樣即使之前的釋放也不會存在問題)。
經(jīng)驗(yàn)之談
不要混用
指針之間的混用,有時候會造成不可預(yù)知的錯誤,所以建議盡量不要混用。包括裸指針和智能指針以及智能指針之間的混用
裸指針和智能指針混用
代碼如下:
void fun() {
auto ptr = new Type;
std::shared_ptr<Type> t(ptr);
delete ptr;
}
在上述代碼中,將ptr所有權(quán)歸shared_ptr所擁有,所以在出fun()函數(shù)作用域的時候,會自動釋放ptr指針,而在函數(shù)末尾又主動調(diào)用delete來釋放,這就會造成double delete,會造成segment fault。
智能指針混用
代碼如下:
void fun() {
std::unique_ptr<Type> t(new Type);
std::shared_ptr<Type> t1(t.get());
}
在上述代碼中,將t關(guān)聯(lián)的對象又給了t1,也就是說同一個對象被兩個智能指針?biāo)鶕碛?,所以在出fun()函數(shù)作用域的時候,二者都會釋放其關(guān)聯(lián)的對象,這就會造成double delete,會造成segment fault。
需要注意的是,下面代碼在STL中是支持的:
void fun() {
std::unique_ptr<Type> t(new Type);
std::shared_ptr<Type> t1(std::move(t));
}
不要管理同一個裸指針
代碼如下:
void fun() {
auto ptr = new Type;
std::unique_ptr<Type> t(ptr);
std::shared_ptr<Type> t1(ptr);
}
在上述代碼中,ptr所有權(quán)同時給了t和t1,也就是說同一個對象被兩個智能指針?biāo)鶕碛校栽诔鰂un()函數(shù)作用域的時候,二者都會釋放其關(guān)聯(lián)的對象,這就會造成double delete,會造成segment fault。
避免使用get()獲取原生指針
void fun(){
auto ptr = std::make_shared<Type>();
auto a= ptr.get();
std::shared_ptr<Type> t(a);
delete a;
}
一般情況下,生成的指針都要顯式調(diào)用delete來進(jìn)行釋放,而上述這種,很容易稍不注意就調(diào)用delete;非必要不要使用get()獲取原生指針。
不要管理this指針
class Type {
private:
void fun() {
std::shared_ptr<Type> t(this);
}
};
在上述代碼中,如果Type在棧上,則會導(dǎo)致segment fault,堆上視實(shí)際情況(如果在對象在堆上生成,那么使用合理的話,是允許的)。
只管理堆上的對象
void fun() {
Type t;
std::shared_ptr<Type> ptr(&t);
};
在上述代碼中,t在棧上進(jìn)行分配,在出作用域的時候,會自動釋放。而ptr在出作用域的時候,也會調(diào)用delete釋放t,而t本身在棧上,delete一個棧上的地址,會造成segment fault。
優(yōu)先使用unique_ptr
根據(jù)業(yè)務(wù)場景,如果需要資源獨(dú)占,那么建議使用unique_ptr而不是shared_ptr,原因如下:
性能優(yōu)于shared_ptr 因?yàn)閟hared_ptr在拷貝或者釋放時候,都需要操作引用計(jì)數(shù) 內(nèi)存占用上小于shared_ptr shared_ptr需要維護(hù)它指向的對象的線程安全引用計(jì)數(shù)和一個控制塊,這使得它比unique_ptr更重量級
使用make_shared初始化
我們看下常用的初始化shared_ptr兩種方式,代碼如下:
std::shared_ptr<Type> p1 = new Type;
std::shared_ptr<Type> p2 = std::make_shared<Type>();
那么,上述兩種方法孰優(yōu)孰劣呢?我們且從源碼的角度進(jìn)行分析。
第一種初始化方法,有兩次內(nèi)存分配:
new Type分配對象 為p1分配控制塊(control block),控制塊用于存放引用計(jì)數(shù)等信息
我們再看下make_shared源碼:
template<class _Ty,
class... _Types> inline
shared_ptr<_Ty> make_shared(_Types&&... _Args)
{ // make a shared_ptr
_Ref_count_obj<_Ty> *_Rx =
new _Ref_count_obj<_Ty>(_STD forward<_Types>(_Args)...);
shared_ptr<_Ty> _Ret;
_Ret._Resetp0(_Rx->_Getptr(), _Rx);
return (_Ret);
}
這里的_Ref_count_obj類包含成員變量:
控制塊 一個內(nèi)存塊,用于存放智能指針管理的資源對象
再看看_Ref_count_obj的構(gòu)造函數(shù):
template<class... _Types>
_Ref_count_obj(_Types&&... _Args)
: _Ref_count_base()
{ // construct from argument list
::new ((void *)&_Storage) _Ty(_STD forward<_Types>(_Args)...);
}
此處雖然也有一個new操作,但是此處是placement new,所以不存在內(nèi)存申請。
從上面分析我們可以看出,第一種初始化方式(new方式)共有兩次內(nèi)存分配操作,而第二種初始化方式(make_shared)只有一次內(nèi)存申請,所以建議使用make_shared方式進(jìn)行初始化。
結(jié)語
智能指針的出現(xiàn),能夠使得開發(fā)者不需要關(guān)心內(nèi)存的釋放,進(jìn)而使得開發(fā)者能夠?qū)⒏嗟木ν度氲綐I(yè)務(wù)上。但是,因?yàn)橹悄苤羔槺旧硪灿衅渚窒扌?,如果使用不?dāng),會造成意想不到的后果,所以,在使用之前,需要做一些必要的檢查,為了更好地用好智能指針,建議看下源碼實(shí)現(xiàn),還是比較簡單的??。
好了,今天的文章就到這,我們下期見。
參考
https://docs.microsoft.com/en-us/previous-versions/visualstudio/visual-studio-2012/hh279676(v=vs.110)?redirectedfrom=MSDN
https://rufflewind.com/2016-03-05/unique-ptr
https://www.nextptr.com/tutorial/ta1450413058/unique_ptr-shared_ptr-weak_ptr-or-reference_wrapper-for-class-relationships
https://gcc.gnu.org/onlinedocs/gcc-4.6.3/libstdc++/api/a01099_source.html
https://gcc.gnu.org/onlinedocs/libstdc++/libstdc++-html-USERS-4.4/a01327.html
https://www.nextptr.com/tutorial/ta1358374985/shared_ptr-basics-and-internals-with-examples
