日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程语言 > c/c++ >内容正文

c/c++

c++ 智能指针_详解 C++ 11 中的智能指针

發(fā)布時間:2024/9/27 c/c++ 24 豆豆
生活随笔 收集整理的這篇文章主要介紹了 c++ 智能指针_详解 C++ 11 中的智能指针 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

C/C++ 語言最為人所詬病的特性之一就是存在內(nèi)存泄露問題,因此后來的大多數(shù)語言都提供了內(nèi)置內(nèi)存分配與釋放功能,有的甚至干脆對語言的使用者屏蔽了內(nèi)存指針這一概念。這里不置貶褒,手動分配內(nèi)存與手動釋放內(nèi)存有利也有弊,自動分配內(nèi)存和自動釋放內(nèi)存亦如此,這是兩種不同的設(shè)計哲學(xué)。有人認(rèn)為,內(nèi)存如此重要的東西怎么能放心交給用戶去管理呢?而另外一些人則認(rèn)為,內(nèi)存如此重要的東西怎么能放心交給系統(tǒng)去管理呢?在 C/C++ 語言中,內(nèi)存泄露的問題一直困擾著廣大的開發(fā)者,因此各類庫和工具的一直在努力嘗試各種方法去檢測和避免內(nèi)存泄露,如 boost,智能指針技術(shù)應(yīng)運而生。

C++ 98/03 的嘗試——std::auto_ptr

在 2019 年討論?std::auto_ptr?不免有點讓人懷疑是不是有點過時了,確實如此,隨著 C++11 標(biāo)準(zhǔn)的出現(xiàn)(最新標(biāo)準(zhǔn)是 C++20),std::auto_ptr?已經(jīng)被徹底廢棄了,取而代之是?std::unique_ptr。然而,我之所以還向你介紹一下?std::auto_ptr?的用法以及它的設(shè)計不足之處是想讓你了解 C++ 語言中智能指針的發(fā)展過程,一項技術(shù)如果我們了解它過去的樣子和發(fā)展的軌跡,我們就能更好地掌握它,不是嗎?

std::auto_ptr?的基本用法如下代碼所示:

#include?

int?main(){
????//初始化方式1
????std::auto_ptr<int>?sp1(new?int(8));
????//初始化方式2
????std::auto_ptr<int>?sp2;
????sp2.reset(new?int(8));

????return?0;
}

智能指針對象?sp1?和?sp2?均持有一個在堆上分配 int 對象,其值均是 8,這兩塊堆內(nèi)存均可以在?sp1?和?sp2?釋放時得到釋放。這是?std::auto_ptr?的基本用法。

sp 是 smart pointer(智能指針)的簡寫。

std::auto_ptr?真正讓人容易誤用的地方是其不常用的復(fù)制語義,即當(dāng)復(fù)制一個?std::auto_ptr?對象時(拷貝復(fù)制或 operator = 復(fù)制),原對象所持有的堆內(nèi)存對象也會轉(zhuǎn)移給復(fù)制出來的對象。示例代碼如下:

#include?
#include?

int?main(){
????//測試拷貝構(gòu)造
????std::auto_ptr<int>?sp1(new?int(8));
????std::auto_ptr<int>?sp2(sp1);
????if?(sp1.get()?!=?NULL)
????{
????????std::cout?<"sp1?is?not?empty."?<std::endl;
????}
????else
????{
????????std::cout?<"sp1?is?empty."?<std::endl;
????}

????if?(sp2.get()?!=?NULL)
????{
????????std::cout?<"sp2?is?not?empty."?<std::endl;
????}
????else
????{
????????std::cout?<"sp2?is?empty."?<std::endl;
????}

????//測試賦值構(gòu)造
????std::auto_ptr<int>?sp3(new?int(8));
????std::auto_ptr<int>?sp4 = sp3;;
????if?(sp3.get()?!=?NULL)
????{
????????std::cout?<"sp3?is?not?empty."?<std::endl;
????}
????else
????{
????????std::cout?<"sp3?is?empty."?<std::endl;
????}

????if?(sp4.get()?!=?NULL)
????{
????????std::cout?<"sp4?is?not?empty."?<std::endl;
????}
????else
????{
????????std::cout?<"sp4?is?empty."?<std::endl;
????}

????return?0;
}

上述代碼中分別利用拷貝構(gòu)造(sp1 => sp2)和 賦值構(gòu)造(sp3 => sp4)來創(chuàng)建新的 std::auto_ptr 對象,因此 sp1 持有的堆對象被轉(zhuǎn)移給 sp2,sp3 持有的堆對象被轉(zhuǎn)移給 sp4。我們得到程序執(zhí)行結(jié)果如下:

[root@iZ238vnojlyZ?testx]#?g++?-g?-o?test_auto_ptr?test_auto_ptr.cpp
[root@iZ238vnojlyZ?testx]#?./test_auto_ptr?
sp1?is?empty.
sp2?is?not?empty.
sp3?is?empty.
sp4?is?not?empty.

由于?std::auto_ptr?這種不常用的復(fù)制語義,我們應(yīng)該避免在 stl 容器中使用?std::auto_ptr,例如我們絕不應(yīng)該寫出如下代碼:

std::vector<std::auto_ptr<int>>?myvectors;

當(dāng)用算法對容器操作的時候(如最常見的容器元素遍歷),很難避免不對容器中的元素實現(xiàn)賦值傳遞,這樣便會使容器中多個元素被置為空指針,這不是我們想看到的,會造成很多意想不到的錯誤。

以史為鑒,作為?std::auto_ptr?的替代者?std::unique_ptr?吸取了這個經(jīng)驗教訓(xùn)。下文會來詳細(xì)介紹。

正因為?std::auto_ptr?的設(shè)計存在如此重大缺陷,C++11 標(biāo)準(zhǔn)在充分借鑒和吸收了 boost 庫中智能指針的設(shè)計思想,引入了三種類型的智能指針,即?std::unique_ptrstd::shared_ptr?和?std::weak_ptr

boost 還有 scoped_ptr,C++11 并沒有全部照搬,而是選擇了三個最實用的指針類型。在 C++11 中可以通過 std::unique_ptr 達(dá)到與 boost::scoped_ptr 一樣的效果。

所有的智能指針類(包括 std::unique_ptr)均包含于頭文件??中。

正因為存在上述設(shè)計上的缺陷,在 C++11及后續(xù)語言規(guī)范中 std::auto_ptr 已經(jīng)被廢棄,你的代碼不應(yīng)該再使用它。

std::unique_ptr

std::unique_ptr?對其持有的堆內(nèi)存具有唯一擁有權(quán),也就是說引用計數(shù)永遠(yuǎn)是 1,std::unique_ptr?對象銷毀時會釋放其持有的堆內(nèi)存。可以使用以下方式初始化一個?std::unique_ptr?對象:

//初始化方式1
std::unique_ptr<int>?sp1(new?int(123));

//初始化方式2
std::unique_ptr<int>?sp2;
sp2.reset(new?int(123));

//初始化方式3
std::unique_ptr<int>?sp3?=?std::make_unique<int>(123);

你應(yīng)該盡量使用初始化方式 3 的方式去創(chuàng)建一個?std::unique_ptr?而不是方式 1 和 2,因為形式 3 更安全,原因 Scott Meyers 在其《Effective Modern C++》中已經(jīng)解釋過了,有興趣的讀者可以閱讀此書相關(guān)章節(jié)。

令很多人對 C++11 規(guī)范不滿的地方是,C++11 新增了 std::make_shared() 方法創(chuàng)建一個 std::shared_ptr 對象,卻沒有提供相應(yīng)的 std::make_unique() 方法創(chuàng)建一個 std::unique_ptr 對象,這個方法直到 C++14 才被添加進來。當(dāng)然,在 C++11 中你很容易實現(xiàn)出這樣一個方法來:

template<typename?T,?typename...?Ts>
std::unique_ptr?make_unique(Ts&&?...params)
{return?std::unique_ptr(new?T(std::forward(params)...));
}

鑒于?std::auto_ptr?的前車之鑒,std::unique_ptr?禁止復(fù)制語義,為了達(dá)到這個效果,std::unique_ptr?類的拷貝構(gòu)造函數(shù)和賦值運算符(operator =)被標(biāo)記為?delete

template?<class?T>class?unique_ptr
{
????//省略其他代碼...

????//拷貝構(gòu)造函數(shù)和賦值運算符被標(biāo)記為delete
????unique_ptr(const?unique_ptr&)?=?delete;
????unique_ptr&?operator=(const?unique_ptr&)?=?delete;
};

因此,下列代碼是無法通過編譯的:

std::unique_ptr<int>?sp1(std::make_unique<int>(123));;

//以下代碼無法通過編譯
//std::unique_ptr?sp2(sp1);
std::unique_ptr<int>?sp3;
//以下代碼無法通過編譯
//sp3?=?sp1;

禁止復(fù)制語義也存在特例,即可以通過一個函數(shù)返回一個 std::unique_ptr:

#include?

std::unique_ptr<int>?func(int?val)
{
????std::unique_ptr<int>?up(new?int(val));
????return?up;
}

int?main(){
????std::unique_ptr<int>?sp1?=?func(123);

????return?0;
}

上述代碼從 func 函數(shù)中得到一個?std::unique_ptr?對象,然后返回給 sp1。

既然?std::unique_ptr?不能復(fù)制,那么如何將一個?std::unique_ptr?對象持有的堆內(nèi)存轉(zhuǎn)移給另外一個呢?答案是使用移動構(gòu)造,示例代碼如下:

#include?

int?main(){
????std::unique_ptr<int>?sp1(std::make_unique<int>(123));

????std::unique_ptr<int>?sp2(std::move(sp1));

????std::unique_ptr<int>?sp3;
????sp3?=?std::move(sp2);

????return?0;
}

以上代碼利用 std::move 將 sp1 持有的堆內(nèi)存(值為 123)轉(zhuǎn)移給 sp2,再把 sp2 轉(zhuǎn)移給 sp3。最后,sp1 和 sp2 不再持有堆內(nèi)存的引用,變成一個空的智能指針對象。并不是所有的對象的 std::move 操作都有意義,只有實現(xiàn)了移動構(gòu)造函數(shù)(Move Constructor)或移動賦值運算符(operator =)的類才行,而?std::unique_ptr?正好實現(xiàn)了這二者,以下是實現(xiàn)偽碼:

template<typename?T,?typename?Deletor>
class?unique_ptr
{
????//其他函數(shù)省略...
public:
????unique_ptr(unique_ptr&&?rhs)
????{
????????this->m_pT?=?rhs.m_pT;
????????//源對象釋放
????????rhs.m_pT?=?nullptr;
????}

????unique_ptr&?operator=(unique_ptr&&?rhs)
????{
????????this->m_pT?=?rhs.m_pT;
????????//源對象釋放
????????rhs.m_pT?=?nullptr;
????????return?*this;
????}

private:
????T*????m_pT;
};

這是?std::unique_ptr?具有移動語義的原因,希望讀者可以理解之。關(guān)于移動構(gòu)造和?std::move,我們將在后面章節(jié)詳細(xì)介紹。

std::unique_ptr?不僅可以持有一個堆對象,也可以持有一組堆對象,示例如下:

#include?
#include?

int?main(){
????//創(chuàng)建10個int類型的堆對象
????//形式1
????std::unique_ptr<int[]>?sp1(new?int[10]);

????//形式2
????std::unique_ptr<int[]>?sp2;
????sp2.reset(new?int[10]);
????//形式3
????std::unique_ptr<int[]>?sp3(std::make_unique<int[]>(10));

????for?(int?i?=?0;?i?10;?++i)
????{
????????sp1[i]?=?i;
????????sp2[i]?=?i;
????????sp3[i]?=?i;
????}

????for?(int?i?=?0;?i?10;?++i)
????{
????????std::cout?<",?"?<",?"?<std::endl;
????}

????return?0;
}

程序執(zhí)行結(jié)果如下:

[root@myaliyun?testmybook]#?g++?-g?-o?test_unique_ptr_with_array?test_unique_ptr_with_array.cpp?-std=c++17
[root@myaliyun?testmybook]#?./test_unique_ptr_with_array?
0,?0,?0
1,?1,?1
2,?2,?2
3,?3,?3
4,?4,?4
5,?5,?5
6,?6,?6
7,?7,?7
8,?8,?8
9,?9,?9

std::shared_ptr?和?std::weak_ptr?也可以持有一組堆對象,用法與?std::unique_ptr?相同,下文不再贅述。

自定義智能指針對象持有的資源的釋放函數(shù)

默認(rèn)情況下,智能指針對象在析構(gòu)時只會釋放其持有的堆內(nèi)存(調(diào)用 delete 或者 delete[]),但是假設(shè)這塊堆內(nèi)存代表的對象還對應(yīng)一種需要回收的資源(如操作系統(tǒng)的套接字句柄、文件句柄等),我們可以通過自定義智能指針的資源釋放函數(shù)。假設(shè)現(xiàn)在有一個 Socket 類,對應(yīng)著操作系統(tǒng)的套接字句柄,在回收時需要關(guān)閉該對象,我們可以如下自定義智能指針對象的資源析構(gòu)函數(shù),這里以?std::unique_ptr?為例:

#include?
#include?

class?Socket
{
public:
????Socket()
????{

????}

????~Socket()
????{

????}

????//關(guān)閉資源句柄
????void?close(){

????}
};

int?main(){
????auto?deletor?=?[](Socket*?pSocket)?{
????????//關(guān)閉句柄
????????pSocket->close();
????????//TODO:?你甚至可以在這里打印一行日志...
????????delete?pSocket;
????};

????std::unique_ptrvoid(*)(Socket?*?pSocket)>?spSocket(new?Socket(),?deletor);return?0;
}

自定義?std::unique_ptr?的資源釋放函數(shù)其規(guī)則是:

std::unique_ptr

其中 T 是你要釋放的對象類型,DeletorPtr 是一個自定義函數(shù)指針。上述代碼?33?行表示 DeletorPtr 有點復(fù)雜,我們可以使用?decltype(deletor)?讓編譯器自己推導(dǎo) deletor 的類型,因此可以將?33?行代碼修改為:

std::unique_ptrdecltype(deletor)>?spSocket(new?Socket(),?deletor);

std::shared_ptr

std::unique_ptr?對其持有的資源具有獨占性,而?std::shared_ptr?持有的資源可以在多個?std::shared_ptr?之間共享,每多一個?std::shared_ptr?對資源的引用,資源引用計數(shù)將增加 1,每一個指向該資源的?std::shared_ptr?對象析構(gòu)時,資源引用計數(shù)減 1,最后一個?std::shared_ptr?對象析構(gòu)時,發(fā)現(xiàn)資源計數(shù)為 0,將釋放其持有的資源。多個線程之間,遞增和減少資源的引用計數(shù)是安全的。(注意:這不意味著多個線程同時操作?std::shared_ptr?引用的對象是安全的)。std::shared_ptr?提供了一個?use_count()?方法來獲取當(dāng)前持有資源的引用計數(shù)。除了上面描述的,std::shared_ptr?用法和?std::unique_ptr?基本相同。

下面是一個初始化?std::shared_ptr?的示例:

//初始化方式1
std::shared_ptr<int>?sp1(new?int(123));

//初始化方式2
std::shared_ptr<int>?sp2;
sp2.reset(new?int(123));

//初始化方式3
std::shared_ptr<int>?sp3;
sp3?=?std::make_shared<int>(123);

和?std::unique_ptr?一樣,你應(yīng)該優(yōu)先使用?std::make_shared?去初始化一個?std::shared_ptr?對象。

再來看另外一段代碼:

#include?
#include?

class?A
{
public:
????A()
????{
????????std::cout?<"A?constructor"?<std::endl;
????}

????~A()
????{
????????std::cout?<"A?destructor"?<std::endl;
????}
};

int?main(){
????{
????????//初始化方式1
????????std::shared_ptr?sp1(new?A());std::cout?<"use?count:?"?<std::endl;//初始化方式2std::shared_ptr?sp2(sp1);std::cout?<"use?count:?"?<std::endl;
????????sp2.reset();std::cout?<"use?count:?"?<std::endl;
????????{std::shared_ptr?sp3?=?sp1;std::cout?<"use?count:?"?<std::endl;
????????}std::cout?<"use?count:?"?<std::endl;
????}return?0;
}
  • 上述代碼?22?行 sp1 構(gòu)造時,同時觸發(fā)對象 A 的構(gòu)造,因此 A 的構(gòu)造函數(shù)會執(zhí)行;

  • 此時只有一個 sp1 對象引用?22?行 new 出來的 A 對象(為了敘述方便,下文統(tǒng)一稱之為資源對象 A),因此代碼?24?行打印出來的引用計數(shù)值為?1

  • 代碼?27?行,利用 sp1 拷貝一份 sp2,導(dǎo)致代碼?28?行打印出來的引用計數(shù)為?2

  • 代碼?30?行調(diào)用 sp2 的 reset() 方法,sp2 釋放對資源對象 A 的引用,因此代碼?31?行打印的引用計數(shù)值再次變?yōu)?1

  • 代碼?34?行 利用 sp1 再次 創(chuàng)建 sp3,因此代碼?35?行打印的引用計數(shù)變?yōu)?2

  • 程序執(zhí)行到?36?行以后,sp3 出了其作用域被析構(gòu),資源 A 的引用計數(shù)遞減 1,因此 代碼?38?行打印的引用計數(shù)為?1

  • 程序執(zhí)行到?39?行以后,sp1 出了其作用域被析構(gòu),在其析構(gòu)時遞減資源 A 的引用計數(shù)至?0,并析構(gòu)資源 A 對象,因此類 A 的析構(gòu)函數(shù)被調(diào)用。

所以整個程序的執(zhí)行結(jié)果如下:

[root@myaliyun?testmybook]#?./test_shared_ptr_use_count?
A?constructoruse?count:?1
use?count:?2
use?count:?1
use?count:?2
use?count:?1
A?destructor

std::enable_shared_from_this

實際開發(fā)中,有時候需要在類中返回包裹當(dāng)前對象(this)的一個?std::shared_ptr?對象給外部使用,C++ 新標(biāo)準(zhǔn)也為我們考慮到了這一點,有如此需求的類只要繼承自?std::enable_shared_from_this?模板對象即可。用法如下:

#include?
#include?

class?A?:?public?std::enable_shared_from_this
{public:
????A()
????{std::cout?<"A?constructor"?<std::endl;
????}
????~A()
????{std::cout?<"A?destructor"?<std::endl;
????}std::shared_ptr?getSelf()
????{return?shared_from_this();
????}
};int?main(){std::shared_ptr?sp1(new?A());std::shared_ptr?sp2?=?sp1->getSelf();std::cout?<"use?count:?"?<std::endl;return?0;
}

上述代碼中,類 A 的繼承?std::enable_shared_from_this?并提供一個?getSelf()?方法返回自身的?std::shared_ptr?對象,在?getSelf()?中調(diào)用?shared_from_this()?即可。

std::enable_shared_from_this?用起來比較方便,但是也存在很多不易察覺的陷阱。

陷阱一:不應(yīng)該共享棧對象的 this 給智能指針對象

假設(shè)我們將上面代碼 main 函數(shù)?25?行生成 A 對象的方式改成一個棧變量,即:

//其他相同代碼省略...

int?main(){
????A?a;

????std::shared_ptr?sp2?=?a.getSelf();std::cout?<"use?count:?"?<std::endl;return?0;
}

運行修改后的代碼會發(fā)現(xiàn)程序在?std::shared_ptr sp2 = a.getSelf();?產(chǎn)生崩潰。這是因為,智能指針管理的是堆對象,棧對象會在函數(shù)調(diào)用結(jié)束后自行銷毀,因此不能通過?shared_from_this()?將該對象交由智能指針對象管理。切記:智能指針最初設(shè)計的目的就是為了管理堆對象的(即那些不會自動釋放的資源)

陷阱二:避免 std::enable_shared_from_this 的循環(huán)引用問題

再來看另外一段代碼:

//?test_std_enable_shared_from_this.cpp?:?This?file?contains?the?'main'?function.?Program?execution?begins?and?ends?there.
//
#include?
#include?

class?A?:?public?std::enable_shared_from_this
{public:
????A()
????{
????????m_i?=?9;//注意://比較好的做法是在構(gòu)造函數(shù)里面調(diào)用shared_from_this()給m_SelfPtr賦值//但是很遺憾不能這么做,如果寫在構(gòu)造函數(shù)里面程序會直接崩潰std::cout?<"A?constructor"?<std::endl;
????}
????~A()
????{
????????m_i?=?0;std::cout?<"A?destructor"?<std::endl;
????}void?func(){
????????m_SelfPtr?=?shared_from_this();
????}public:int?????????????????m_i;std::shared_ptr??m_SelfPtr;
};int?main(){
????{std::shared_ptr?spa(new?A());
????????spa->func();
????}return?0;
}

乍一看上面的代碼好像看不出什么問題,讓我們來實際運行一下看看輸出結(jié)果:

[root@myaliyun?testmybook]#?g++?-g?-o?test_std_enable_shared_from_this_problem?test_std_enable_shared_from_this_problem.cpp
[root@myaliyun?testmybook]#?./test_std_enable_shared_from_this_problem
A?constructor

我們發(fā)現(xiàn)在程序的整個生命周期內(nèi),只有 A 類構(gòu)造函數(shù)的調(diào)用輸出,沒有 A 類析構(gòu)函數(shù)的調(diào)用輸出,這意味著 new 出來的 A 對象產(chǎn)生了內(nèi)存泄漏了!

我們來分析一下為什么 new 出來的 A 對象得不到釋放。當(dāng)程序執(zhí)行到?42?行后,spa 出了其作用域準(zhǔn)備析構(gòu),在析構(gòu)時其發(fā)現(xiàn)仍然有另外的一個 std::shared_ptr 對象即 A::m_SelfPtr 引用了 A,因此 spa 只會將 A 的引用計數(shù)遞減為 1,然后就銷毀自身了。現(xiàn)在留下一個矛盾的處境:必須銷毀 A 才能銷毀其成員變量 m_SelfPtr,而銷毀 m_SelfPtr 必須先銷毀 A。這就是所謂的 std::enable_shared_from_this 的循環(huán)引用問題。我們在實際開發(fā)中應(yīng)該避免做出這樣的邏輯設(shè)計,這種情形下即使使用了智能指針也會造成內(nèi)存泄漏。也就是說一個資源的生命周期可以交給一個智能指針對象,但是該智能指針的生命周期不可以再交給整個資源來管理。

std::weak_ptr

std::weak_ptr?是一個不控制資源生命周期的智能指針,是對對象的一種弱引用,只是提供了對其管理的資源的一個訪問手段,引入它的目的為協(xié)助?std::shared_ptr?工作。

std::weak_ptr?可以從一個?std::shared_ptr?或另一個?std::weak_ptr?對象構(gòu)造,std::shared_ptr?可以直接賦值給?std::weak_ptr?,也可以通過?std::weak_ptr?的?lock()?函數(shù)來獲得?std::shared_ptr。它的構(gòu)造和析構(gòu)不會引起引用計數(shù)的增加或減少。std::weak_ptr?可用來解決?std::shared_ptr?相互引用時的死鎖問題(即兩個std::shared_ptr?相互引用,那么這兩個指針的引用計數(shù)永遠(yuǎn)不可能下降為 0, 資源永遠(yuǎn)不會釋放)。

示例代碼如下:

#include?
#include?

int?main(){
????//創(chuàng)建一個std::shared_ptr對象
????std::shared_ptr<int>?sp1(new?int(123));
????std::cout?<"use?count:?"?<std::endl;

????//通過構(gòu)造函數(shù)得到一個std::weak_ptr對象
????std::weak_ptr<int>?sp2(sp1);
????std::cout?<"use?count:?"?<std::endl;

????//通過賦值運算符得到一個std::weak_ptr對象
????std::weak_ptr<int>?sp3?=?sp1;
????std::cout?<"use?count:?"?<std::endl;

????//通過一個std::weak_ptr對象得到另外一個std::weak_ptr對象
????std::weak_ptr<int>?sp4?=?sp2;
????std::cout?<"use?count:?"?<std::endl;

????return?0;
}

程序執(zhí)行結(jié)果如下:

[root@myaliyun?testmybook]#?g++?-g?-o?test_weak_ptr?test_weak_ptr.cpp?
[root@myaliyun?testmybook]#?./test_weak_ptr
use?count:?1
use?count:?1
use?count:?1
use?count:?1

無論通過何種方式創(chuàng)建?std::weak_ptr?都不會增加資源的引用計數(shù),因此每次輸出引用計數(shù)的值都是 1。

既然,std::weak_ptr?不管理對象的生命周期,那么其引用的對象可能在某個時刻被銷毀了,如何得知呢?std::weak_ptr?提供了一個?expired()?方法來做這一項檢測,返回 true,說明其引用的資源已經(jīng)不存在了;返回 false,說明該資源仍然存在,這個時候可以使用?std::weak_ptr?的?lock()?方法得到一個?std::shared_ptr?對象然后繼續(xù)操作資源,以下代碼演示了該用法:

//tmpConn_?是一個?std::weak_ptr?對象
//tmpConn_引用的TcpConnection已經(jīng)銷毀,直接返回
if?(tmpConn_.expired())
????return;

std::shared_ptr?conn?=?tmpConn_.lock();if?(conn)
{//對conn進行操作,省略...
}

有讀者可能對上述代碼產(chǎn)生疑問,既然使用了?std::weak_ptr?的?expired()?方法判斷了對象是否存在,為什么不直接使用?std::weak_ptr?對象對引用資源進行操作呢?實際上這是行不通的,std::weak_ptr?類沒有重寫?operator->?和?operator* 方法,因此不能像?std::shared_ptr?或?std::unique_ptr?一樣直接操作對象,同時?std::weak_ptr?類也沒有重寫?operator!?操作,因此也不能通過?std::weak_ptr?對象直接判斷其引用的資源是否存在:

#include?

class?A
{
public:
????void?doSomething(){

????}
};

int?main(){????
????std::shared_ptr?sp1(new?A());std::weak_ptr?sp2(sp1);//正確代碼if?(sp1)
????{//正確代碼
????????sp1->doSomething();
????????(*sp1).doSomething();
????}//正確代碼if?(!sp1)
????{
????}//錯誤代碼,無法編譯通過//if?(sp2)//{//????//錯誤代碼,無法編譯通過//????sp2->doSomething();//????(*sp2).doSomething();//}//錯誤代碼,無法編譯通過//if?(!sp2)//{//}return?0;
}

之所以?std::weak_ptr?不增加引用資源的引用計數(shù)不管理資源的生命周期,是因為,即使它實現(xiàn)了以上說的幾個方法,調(diào)用它們也是不安全的,因為在調(diào)用期間,引用的資源可能恰好被銷毀了,這會造成棘手的錯誤和麻煩。

因此,std::weak_ptr?的正確使用場景是那些資源如果可能就使用,如果不可使用則不用的場景,它不參與資源的生命周期管理。例如,網(wǎng)絡(luò)分層結(jié)構(gòu)中,Session 對象(會話對象)利用 Connection 對象(連接對象)提供的服務(wù)工作,但是 Session 對象不管理 Connection 對象的生命周期,Session 管理 Connection 的生命周期是不合理的,因為網(wǎng)絡(luò)底層出錯會導(dǎo)致 Connection 對象被銷毀,此時 Session 對象如果強行持有 Connection 對象與事實矛盾。

std::weak_ptr?的應(yīng)用場景,經(jīng)典的例子是訂閱者模式或者觀察者模式中。這里以訂閱者為例來說明,消息發(fā)布器只有在某個訂閱者存在的情況下才會向其發(fā)布消息,而不能管理訂閱者的生命周期。

class?Subscriber
{

};

class?SubscribeManager
{
public:
????void?publish(){
????????for?(const?auto&?iter?:?m_subscribers)
????????{
????????????if?(!iter.expired())
????????????{
????????????????//TODO:給訂閱者發(fā)送消息
????????????}
????????}
????}

private:
????std::vector<std::weak_ptr>???m_subscribers;
};

智能指針對象的大小

一個?std::unique_ptr?對象大小與裸指針大小相同(即 sizeof(std::unique_ptr) == sizeof(void*)),而?std::shared_ptr?的大小是?std::unique_ptr?的一倍。以下是我分別在 Visual Studio 2019 和 gcc/g++ 4.8 上(二者都編譯成 x64 程序)的測試結(jié)果:

測試代碼

#include?
#include?
#include?

int?main(){
????std::shared_ptr<int>?sp0;
????std::shared_ptr<std::string>?sp1;
????sp1.reset(new?std::string());
????std::unique_ptr<int>?sp2;
????std::weak_ptr<int>?sp3;

????std::cout?<"sp0?size:?"?<sizeof(sp0)?<std::endl;
????std::cout?<"sp1?size:?"?<sizeof(sp1)?<std::endl;
????std::cout?<"sp2?size:?"?<sizeof(sp2)?<std::endl;
????std::cout?<"sp3?size:?"?<sizeof(sp3)?<std::endl;

????return?0;
}

Visual Studio 2019 運行結(jié)果:

gcc/g++ 運行結(jié)果:

在 32 位機器上,std_unique_ptr?占 4 字節(jié),std::shared_ptr?和?std::weak_ptr?占 8 字節(jié);在 64 位機器上,std_unique_ptr?占 8 字節(jié),std::shared_ptr?和?std::weak_ptr?占 16 字節(jié)。也就是說,std_unique_ptr?的大小總是和原始指針大小一樣,std::shared_ptr?和?std::weak_ptr?大小是原始指針的一倍。

智能指針使用注意事項

C++ 新標(biāo)準(zhǔn)提倡的理念之一是不應(yīng)該再手動調(diào)用 delete 或者 free 函數(shù)去釋放內(nèi)存了,而應(yīng)該把它們交給新標(biāo)準(zhǔn)提供的各種智能指針對象。C++ 新標(biāo)準(zhǔn)中的各種智能指針是如此的實用與強大,在現(xiàn)代 C++ 項目開發(fā)中,讀者應(yīng)該盡量去使用它們。智能指針雖然好用,但稍不注意,也可能存在許多難以發(fā)現(xiàn)的 bug,這里我根據(jù)經(jīng)驗總結(jié)了幾條:

  • 一旦一個對象使用智能指針管理后,就不該再使用原始裸指針去操作;

    看一段代碼:

    #include?

    class?Subscriber
    {

    };

    int?main(){????
    ??Subscriber*?pSubscriber?=?new?Subscriber();

    ??std::unique_ptr?spSubscriber(pSubscriber);delete?pSubscriber;return?0;
    }

    這段代碼利用創(chuàng)建了一個堆對象 Subscriber,然后利用智能指針 spSubscriber 去管理之,可以卻私下利用原始指針銷毀了該對象,這讓智能指針對象?spSubscriber?情何以堪啊?

    記住,一旦智能指針對象接管了你的資源,所有對資源的操作都應(yīng)該通過智能指針對象進行,不建議再通過原始指針進行操作了。當(dāng)然,除了?std::weak_ptrstd::unique_ptr?和?std::shared_ptr?都提供了獲取原始指針的方法——get()?函數(shù)。

    int?main(){????
    ??Subscriber*?pSubscriber?=?new?Subscriber();

    ??std::unique_ptr?spSubscriber(pSubscriber);//pTheSameSubscriber和pSubscriber指向同一個對象
    ??Subscriber*?pTheSameSubscriber=?spSubscriber.get();return?0;
    }
  • 分清楚場合應(yīng)該使用哪種類型的智能指針;

    通常情況下,如果你的資源不需要在其他地方共享,那么應(yīng)該優(yōu)先使用?std::unique_ptr,反之使用?std::shared_ptr,當(dāng)然這是在該智能指針需要管理資源的生命周期的情況下;如果不需要管理對象的生命周期,請使用?std::weak_ptr

  • 認(rèn)真考慮,避免操作某個引用資源已經(jīng)釋放的智能指針;

    前面的例子,一定讓你覺得非常容易知道一個智能指針的持有的資源是否還有效,但是還是建議在不同場景謹(jǐn)慎一點,有些場景是很容易造成誤判。例如下面的代碼:

    #include?
    #include?

    class?T
    {
    public:
    ??void?doSomething(){
    ??????std::cout?<"T?do?something..."?<std::endl;
    ??}

    private:
    ??int?????m_i;
    };

    int?main(){????
    ??std::shared_ptr?sp1(new?T());const?auto&?sp2?=?sp1;
    ??sp1.reset();//由于sp2已經(jīng)不再持有對象的引用,程序會在這里出現(xiàn)意外的行為
    ??sp2->doSomething();return?0;
    }

    上述代碼中,sp2 是 sp1 的引用,sp1 被置空后,sp2 也一同為空。這時候調(diào)用 sp2->doSomething(),sp2->(即?operator->)在內(nèi)部會調(diào)用?get()?方法獲取原始指針對象,這時會得到一個空指針(地址為 0),繼續(xù)調(diào)用 doSomething() 導(dǎo)致程序崩潰。

    你一定仍然覺得這個例子也能很明顯地看出問題,ok,讓我們把這個例子放到實際開發(fā)中再來看一下:

    //連接斷開
    void?MonitorServer::OnClose(const?std::shared_ptr&?conn)
    {????std::lock_guard<std::mutex>?guard(m_sessionMutex);for?(auto?iter?=?m_sessions.begin();?iter?!=?m_sessions.end();?++iter)
    ??{//通過比對connection對象找到對應(yīng)的sessionif?((*iter)->GetConnectionPtr()?==?conn)
    ??????{
    ??????????m_sessions.erase(iter);//注意這里:程序在此處崩潰
    ??????????LOGI("monitor?client?disconnected:?%s",?conn->peerAddress().toIpPort().c_str());break;
    ??????}
    ??}
    }

    這段代碼不是我杜撰的,而是來自于我實際的一個商業(yè)項目中。注意代碼中我提醒注意的地方,該段程序會在代碼?12?行處崩潰,崩潰原因是調(diào)用了?conn->peerAddress()?方法。為什么這個方法的調(diào)用可能會引起崩潰?現(xiàn)在可以一目了然地看出了嗎?

    崩潰原因是傳入的 conn 對象和上一個例子中的 sp2 一樣都是另外一個?std::shared_ptr?的引用,當(dāng)連接斷開時,對應(yīng)的 TcpConnection 對象可能早已被銷毀,而 conn 引用就會變成空指針(嚴(yán)格來說是不再擁有一個 TcpConnection 對象),此時調(diào)用 TcpConnection 的 peerAddress() 方法就會產(chǎn)生和上一個示例一樣的錯誤。

  • 作為類成員變量時,應(yīng)該優(yōu)先使用前置聲明(forward declarations)

    我們知道,為了減小編譯依賴加快編譯速度和生成二進制文件的大小,C/C++ 項目中一般在 *.h 文件對于指針類型盡量使用前置聲明,而不是直接包含對應(yīng)類的頭文件。例如:

    //Test.h
    //在這里使用A的前置聲明,而不是直接包含A.h文件
    class?A;

    class?Test
    {
    public:
    ??Test();
    ??~Test();

    private:
    ??A*??????m_pA;
    };

    同樣的道理,在頭文件中當(dāng)使用智能指針對象作為類成員變量時,也應(yīng)該優(yōu)先使用前置聲明去引用智能指針對象的包裹類,而不是直接包含包裹類的頭文件。

    //Test.h
    #include?

    //智能指針包裹類A,這里優(yōu)先使用A的前置聲明,而不是直接包含A.h
    class?A;

    class?Test
    {
    public:
    ??Test();
    ??~Test();

    private:??
    ??std::unique_ptr??m_spA;
    };

C++ 新標(biāo)準(zhǔn)中的智能指針我想介紹的就這么多了,Modern C/C++ 已經(jīng)變?yōu)?C/C++ 開發(fā)的趨勢,希望讀者能善用和熟練使用本節(jié)介紹的后三種智能指針對象。

歷史推薦

1.?聊一聊程序員如何增加收入

2. 這一次我讓你徹底搞明白并發(fā)編程

3. 小方說服務(wù)器開發(fā)——一個實實在在幫你提高后端開發(fā)能力的地方

4. 一個 WebSocket 服務(wù)器是如何開發(fā)出來的?

5.?心跳包設(shè)計機制詳解

歡迎關(guān)注『高性能服務(wù)器開發(fā)』公眾號,一起交流服務(wù)器編程技藝與思想,也可以加入高性能服務(wù)器開發(fā)交流群:578019391?一起交流。

如果覺得對您有幫助,點擊“在看”支持下吧!

總結(jié)

以上是生活随笔為你收集整理的c++ 智能指针_详解 C++ 11 中的智能指针的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。