c++ 智能指针_详解 C++ 11 中的智能指针
C/C++ 語言最為人所詬病的特性之一就是存在內存泄露問題,因此后來的大多數語言都提供了內置內存分配與釋放功能,有的甚至干脆對語言的使用者屏蔽了內存指針這一概念。這里不置貶褒,手動分配內存與手動釋放內存有利也有弊,自動分配內存和自動釋放內存亦如此,這是兩種不同的設計哲學。有人認為,內存如此重要的東西怎么能放心交給用戶去管理呢?而另外一些人則認為,內存如此重要的東西怎么能放心交給系統去管理呢?在 C/C++ 語言中,內存泄露的問題一直困擾著廣大的開發者,因此各類庫和工具的一直在努力嘗試各種方法去檢測和避免內存泄露,如 boost,智能指針技術應運而生。
C++ 98/03 的嘗試——std::auto_ptr
在 2019 年討論?std::auto_ptr?不免有點讓人懷疑是不是有點過時了,確實如此,隨著 C++11 標準的出現(最新標準是 C++20),std::auto_ptr?已經被徹底廢棄了,取而代之是?std::unique_ptr。然而,我之所以還向你介紹一下?std::auto_ptr?的用法以及它的設計不足之處是想讓你了解 C++ 語言中智能指針的發展過程,一項技術如果我們了解它過去的樣子和發展的軌跡,我們就能更好地掌握它,不是嗎?
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,這兩塊堆內存均可以在?sp1?和?sp2?釋放時得到釋放。這是?std::auto_ptr?的基本用法。
sp 是 smart pointer(智能指針)的簡寫。
std::auto_ptr?真正讓人容易誤用的地方是其不常用的復制語義,即當復制一個?std::auto_ptr?對象時(拷貝復制或 operator = 復制),原對象所持有的堆內存對象也會轉移給復制出來的對象。示例代碼如下:
#include?#include?
int?main(){
????//測試拷貝構造
????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;
????}
????//測試賦值構造
????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;
}
上述代碼中分別利用拷貝構造(sp1 => sp2)和 賦值構造(sp3 => sp4)來創建新的 std::auto_ptr 對象,因此 sp1 持有的堆對象被轉移給 sp2,sp3 持有的堆對象被轉移給 sp4。我們得到程序執行結果如下:
[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?這種不常用的復制語義,我們應該避免在 stl 容器中使用?std::auto_ptr,例如我們絕不應該寫出如下代碼:
std::vector<std::auto_ptr<int>>?myvectors;當用算法對容器操作的時候(如最常見的容器元素遍歷),很難避免不對容器中的元素實現賦值傳遞,這樣便會使容器中多個元素被置為空指針,這不是我們想看到的,會造成很多意想不到的錯誤。
以史為鑒,作為?std::auto_ptr?的替代者?std::unique_ptr?吸取了這個經驗教訓。下文會來詳細介紹。
正因為?std::auto_ptr?的設計存在如此重大缺陷,C++11 標準在充分借鑒和吸收了 boost 庫中智能指針的設計思想,引入了三種類型的智能指針,即?std::unique_ptr、std::shared_ptr?和?std::weak_ptr。
boost 還有 scoped_ptr,C++11 并沒有全部照搬,而是選擇了三個最實用的指針類型。在 C++11 中可以通過 std::unique_ptr 達到與 boost::scoped_ptr 一樣的效果。
所有的智能指針類(包括 std::unique_ptr)均包含于頭文件??中。
正因為存在上述設計上的缺陷,在 C++11及后續語言規范中 std::auto_ptr 已經被廢棄,你的代碼不應該再使用它。
std::unique_ptr
std::unique_ptr?對其持有的堆內存具有唯一擁有權,也就是說引用計數永遠是 1,std::unique_ptr?對象銷毀時會釋放其持有的堆內存。可以使用以下方式初始化一個?std::unique_ptr?對象:
//初始化方式1std::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);
你應該盡量使用初始化方式 3 的方式去創建一個?std::unique_ptr?而不是方式 1 和 2,因為形式 3 更安全,原因 Scott Meyers 在其《Effective Modern C++》中已經解釋過了,有興趣的讀者可以閱讀此書相關章節。
令很多人對 C++11 規范不滿的地方是,C++11 新增了 std::make_shared() 方法創建一個 std::shared_ptr 對象,卻沒有提供相應的 std::make_unique() 方法創建一個 std::unique_ptr 對象,這個方法直到 C++14 才被添加進來。當然,在 C++11 中你很容易實現出這樣一個方法來:
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?禁止復制語義,為了達到這個效果,std::unique_ptr?類的拷貝構造函數和賦值運算符(operator =)被標記為?delete。
template?<class?T>class?unique_ptr{
????//省略其他代碼...
????//拷貝構造函數和賦值運算符被標記為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;
禁止復制語義也存在特例,即可以通過一個函數返回一個 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 函數中得到一個?std::unique_ptr?對象,然后返回給 sp1。
既然?std::unique_ptr?不能復制,那么如何將一個?std::unique_ptr?對象持有的堆內存轉移給另外一個呢?答案是使用移動構造,示例代碼如下:
#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 持有的堆內存(值為 123)轉移給 sp2,再把 sp2 轉移給 sp3。最后,sp1 和 sp2 不再持有堆內存的引用,變成一個空的智能指針對象。并不是所有的對象的 std::move 操作都有意義,只有實現了移動構造函數(Move Constructor)或移動賦值運算符(operator =)的類才行,而?std::unique_ptr?正好實現了這二者,以下是實現偽碼:
template<typename?T,?typename?Deletor>class?unique_ptr
{
????//其他函數省略...
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?具有移動語義的原因,希望讀者可以理解之。關于移動構造和?std::move,我們將在后面章節詳細介紹。
std::unique_ptr?不僅可以持有一個堆對象,也可以持有一組堆對象,示例如下:
#include?#include?
int?main(){
????//創建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;
}
程序執行結果如下:
[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?相同,下文不再贅述。
自定義智能指針對象持有的資源的釋放函數
默認情況下,智能指針對象在析構時只會釋放其持有的堆內存(調用 delete 或者 delete[]),但是假設這塊堆內存代表的對象還對應一種需要回收的資源(如操作系統的套接字句柄、文件句柄等),我們可以通過自定義智能指針的資源釋放函數。假設現在有一個 Socket 類,對應著操作系統的套接字句柄,在回收時需要關閉該對象,我們可以如下自定義智能指針對象的資源析構函數,這里以?std::unique_ptr?為例:
#include?#include?
class?Socket
{
public:
????Socket()
????{
????}
????~Socket()
????{
????}
????//關閉資源句柄
????void?close(){
????}
};
int?main(){
????auto?deletor?=?[](Socket*?pSocket)?{
????????//關閉句柄
????????pSocket->close();
????????//TODO:?你甚至可以在這里打印一行日志...
????????delete?pSocket;
????};
????std::unique_ptrvoid(*)(Socket?*?pSocket)>?spSocket(new?Socket(),?deletor);return?0;
}
自定義?std::unique_ptr?的資源釋放函數其規則是:
std::unique_ptr其中 T 是你要釋放的對象類型,DeletorPtr 是一個自定義函數指針。上述代碼?33?行表示 DeletorPtr 有點復雜,我們可以使用?decltype(deletor)?讓編譯器自己推導 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?對資源的引用,資源引用計數將增加 1,每一個指向該資源的?std::shared_ptr?對象析構時,資源引用計數減 1,最后一個?std::shared_ptr?對象析構時,發現資源計數為 0,將釋放其持有的資源。多個線程之間,遞增和減少資源的引用計數是安全的。(注意:這不意味著多個線程同時操作?std::shared_ptr?引用的對象是安全的)。std::shared_ptr?提供了一個?use_count()?方法來獲取當前持有資源的引用計數。除了上面描述的,std::shared_ptr?用法和?std::unique_ptr?基本相同。
下面是一個初始化?std::shared_ptr?的示例:
//初始化方式1std::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?一樣,你應該優先使用?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 構造時,同時觸發對象 A 的構造,因此 A 的構造函數會執行;
此時只有一個 sp1 對象引用?22?行 new 出來的 A 對象(為了敘述方便,下文統一稱之為資源對象 A),因此代碼?24?行打印出來的引用計數值為?1;
代碼?27?行,利用 sp1 拷貝一份 sp2,導致代碼?28?行打印出來的引用計數為?2;
代碼?30?行調用 sp2 的 reset() 方法,sp2 釋放對資源對象 A 的引用,因此代碼?31?行打印的引用計數值再次變為?1;
代碼?34?行 利用 sp1 再次 創建 sp3,因此代碼?35?行打印的引用計數變為?2;
程序執行到?36?行以后,sp3 出了其作用域被析構,資源 A 的引用計數遞減 1,因此 代碼?38?行打印的引用計數為?1;
程序執行到?39?行以后,sp1 出了其作用域被析構,在其析構時遞減資源 A 的引用計數至?0,并析構資源 A 對象,因此類 A 的析構函數被調用。
所以整個程序的執行結果如下:
[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
實際開發中,有時候需要在類中返回包裹當前對象(this)的一個?std::shared_ptr?對象給外部使用,C++ 新標準也為我們考慮到了這一點,有如此需求的類只要繼承自?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()?中調用?shared_from_this()?即可。
std::enable_shared_from_this?用起來比較方便,但是也存在很多不易察覺的陷阱。
陷阱一:不應該共享棧對象的 this 給智能指針對象
假設我們將上面代碼 main 函數?25?行生成 A 對象的方式改成一個棧變量,即:
//其他相同代碼省略...int?main(){
????A?a;
????std::shared_ptr?sp2?=?a.getSelf();std::cout?<"use?count:?"?<std::endl;return?0;
}
運行修改后的代碼會發現程序在?std::shared_ptr sp2 = a.getSelf();?產生崩潰。這是因為,智能指針管理的是堆對象,棧對象會在函數調用結束后自行銷毀,因此不能通過?shared_from_this()?將該對象交由智能指針對象管理。切記:智能指針最初設計的目的就是為了管理堆對象的(即那些不會自動釋放的資源)。
陷阱二:避免 std::enable_shared_from_this 的循環引用問題
再來看另外一段代碼:
//?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;//注意://比較好的做法是在構造函數里面調用shared_from_this()給m_SelfPtr賦值//但是很遺憾不能這么做,如果寫在構造函數里面程序會直接崩潰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;
}
乍一看上面的代碼好像看不出什么問題,讓我們來實際運行一下看看輸出結果:
[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
我們發現在程序的整個生命周期內,只有 A 類構造函數的調用輸出,沒有 A 類析構函數的調用輸出,這意味著 new 出來的 A 對象產生了內存泄漏了!
我們來分析一下為什么 new 出來的 A 對象得不到釋放。當程序執行到?42?行后,spa 出了其作用域準備析構,在析構時其發現仍然有另外的一個 std::shared_ptr 對象即 A::m_SelfPtr 引用了 A,因此 spa 只會將 A 的引用計數遞減為 1,然后就銷毀自身了。現在留下一個矛盾的處境:必須銷毀 A 才能銷毀其成員變量 m_SelfPtr,而銷毀 m_SelfPtr 必須先銷毀 A。這就是所謂的 std::enable_shared_from_this 的循環引用問題。我們在實際開發中應該避免做出這樣的邏輯設計,這種情形下即使使用了智能指針也會造成內存泄漏。也就是說一個資源的生命周期可以交給一個智能指針對象,但是該智能指針的生命周期不可以再交給整個資源來管理。
std::weak_ptr
std::weak_ptr?是一個不控制資源生命周期的智能指針,是對對象的一種弱引用,只是提供了對其管理的資源的一個訪問手段,引入它的目的為協助?std::shared_ptr?工作。
std::weak_ptr?可以從一個?std::shared_ptr?或另一個?std::weak_ptr?對象構造,std::shared_ptr?可以直接賦值給?std::weak_ptr?,也可以通過?std::weak_ptr?的?lock()?函數來獲得?std::shared_ptr。它的構造和析構不會引起引用計數的增加或減少。std::weak_ptr?可用來解決?std::shared_ptr?相互引用時的死鎖問題(即兩個std::shared_ptr?相互引用,那么這兩個指針的引用計數永遠不可能下降為 0, 資源永遠不會釋放)。
示例代碼如下:
#include?#include?
int?main(){
????//創建一個std::shared_ptr對象
????std::shared_ptr<int>?sp1(new?int(123));
????std::cout?<"use?count:?"?<std::endl;
????//通過構造函數得到一個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;
}
程序執行結果如下:
[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
無論通過何種方式創建?std::weak_ptr?都不會增加資源的引用計數,因此每次輸出引用計數的值都是 1。
既然,std::weak_ptr?不管理對象的生命周期,那么其引用的對象可能在某個時刻被銷毀了,如何得知呢?std::weak_ptr?提供了一個?expired()?方法來做這一項檢測,返回 true,說明其引用的資源已經不存在了;返回 false,說明該資源仍然存在,這個時候可以使用?std::weak_ptr?的?lock()?方法得到一個?std::shared_ptr?對象然后繼續操作資源,以下代碼演示了該用法:
//tmpConn_?是一個?std::weak_ptr?對象//tmpConn_引用的TcpConnection已經銷毀,直接返回
if?(tmpConn_.expired())
????return;
std::shared_ptr?conn?=?tmpConn_.lock();if?(conn)
{//對conn進行操作,省略...
}
有讀者可能對上述代碼產生疑問,既然使用了?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?不增加引用資源的引用計數不管理資源的生命周期,是因為,即使它實現了以上說的幾個方法,調用它們也是不安全的,因為在調用期間,引用的資源可能恰好被銷毀了,這會造成棘手的錯誤和麻煩。
因此,std::weak_ptr?的正確使用場景是那些資源如果可能就使用,如果不可使用則不用的場景,它不參與資源的生命周期管理。例如,網絡分層結構中,Session 對象(會話對象)利用 Connection 對象(連接對象)提供的服務工作,但是 Session 對象不管理 Connection 對象的生命周期,Session 管理 Connection 的生命周期是不合理的,因為網絡底層出錯會導致 Connection 對象被銷毀,此時 Session 對象如果強行持有 Connection 對象與事實矛盾。
std::weak_ptr?的應用場景,經典的例子是訂閱者模式或者觀察者模式中。這里以訂閱者為例來說明,消息發布器只有在某個訂閱者存在的情況下才會向其發布消息,而不能管理訂閱者的生命周期。
class?Subscriber{
};
class?SubscribeManager
{
public:
????void?publish(){
????????for?(const?auto&?iter?:?m_subscribers)
????????{
????????????if?(!iter.expired())
????????????{
????????????????//TODO:給訂閱者發送消息
????????????}
????????}
????}
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 程序)的測試結果:
測試代碼
#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 運行結果:
gcc/g++ 運行結果:
在 32 位機器上,std_unique_ptr?占 4 字節,std::shared_ptr?和?std::weak_ptr?占 8 字節;在 64 位機器上,std_unique_ptr?占 8 字節,std::shared_ptr?和?std::weak_ptr?占 16 字節。也就是說,std_unique_ptr?的大小總是和原始指針大小一樣,std::shared_ptr?和?std::weak_ptr?大小是原始指針的一倍。
智能指針使用注意事項
C++ 新標準提倡的理念之一是不應該再手動調用 delete 或者 free 函數去釋放內存了,而應該把它們交給新標準提供的各種智能指針對象。C++ 新標準中的各種智能指針是如此的實用與強大,在現代 C++ 項目開發中,讀者應該盡量去使用它們。智能指針雖然好用,但稍不注意,也可能存在許多難以發現的 bug,這里我根據經驗總結了幾條:
一旦一個對象使用智能指針管理后,就不該再使用原始裸指針去操作;
看一段代碼:
#include?
class?Subscriber
{
};
int?main(){????
??Subscriber*?pSubscriber?=?new?Subscriber();
??std::unique_ptr?spSubscriber(pSubscriber);delete?pSubscriber;return?0;
}這段代碼利用創建了一個堆對象 Subscriber,然后利用智能指針 spSubscriber 去管理之,可以卻私下利用原始指針銷毀了該對象,這讓智能指針對象?spSubscriber?情何以堪啊?
記住,一旦智能指針對象接管了你的資源,所有對資源的操作都應該通過智能指針對象進行,不建議再通過原始指針進行操作了。當然,除了?std::weak_ptr,std::unique_ptr?和?std::shared_ptr?都提供了獲取原始指針的方法——get()?函數。
int?main(){????
??Subscriber*?pSubscriber?=?new?Subscriber();
??std::unique_ptr?spSubscriber(pSubscriber);//pTheSameSubscriber和pSubscriber指向同一個對象
??Subscriber*?pTheSameSubscriber=?spSubscriber.get();return?0;
}分清楚場合應該使用哪種類型的智能指針;
通常情況下,如果你的資源不需要在其他地方共享,那么應該優先使用?std::unique_ptr,反之使用?std::shared_ptr,當然這是在該智能指針需要管理資源的生命周期的情況下;如果不需要管理對象的生命周期,請使用?std::weak_ptr。
認真考慮,避免操作某個引用資源已經釋放的智能指針;
前面的例子,一定讓你覺得非常容易知道一個智能指針的持有的資源是否還有效,但是還是建議在不同場景謹慎一點,有些場景是很容易造成誤判。例如下面的代碼:
#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已經不再持有對象的引用,程序會在這里出現意外的行為
??sp2->doSomething();return?0;
}上述代碼中,sp2 是 sp1 的引用,sp1 被置空后,sp2 也一同為空。這時候調用 sp2->doSomething(),sp2->(即?operator->)在內部會調用?get()?方法獲取原始指針對象,這時會得到一個空指針(地址為 0),繼續調用 doSomething() 導致程序崩潰。
你一定仍然覺得這個例子也能很明顯地看出問題,ok,讓我們把這個例子放到實際開發中再來看一下:
//連接斷開
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對象找到對應的sessionif?((*iter)->GetConnectionPtr()?==?conn)
??????{
??????????m_sessions.erase(iter);//注意這里:程序在此處崩潰
??????????LOGI("monitor?client?disconnected:?%s",?conn->peerAddress().toIpPort().c_str());break;
??????}
??}
}這段代碼不是我杜撰的,而是來自于我實際的一個商業項目中。注意代碼中我提醒注意的地方,該段程序會在代碼?12?行處崩潰,崩潰原因是調用了?conn->peerAddress()?方法。為什么這個方法的調用可能會引起崩潰?現在可以一目了然地看出了嗎?
崩潰原因是傳入的 conn 對象和上一個例子中的 sp2 一樣都是另外一個?std::shared_ptr?的引用,當連接斷開時,對應的 TcpConnection 對象可能早已被銷毀,而 conn 引用就會變成空指針(嚴格來說是不再擁有一個 TcpConnection 對象),此時調用 TcpConnection 的 peerAddress() 方法就會產生和上一個示例一樣的錯誤。
作為類成員變量時,應該優先使用前置聲明(forward declarations)
我們知道,為了減小編譯依賴加快編譯速度和生成二進制文件的大小,C/C++ 項目中一般在 *.h 文件對于指針類型盡量使用前置聲明,而不是直接包含對應類的頭文件。例如:
//Test.h
//在這里使用A的前置聲明,而不是直接包含A.h文件
class?A;
class?Test
{
public:
??Test();
??~Test();
private:
??A*??????m_pA;
};同樣的道理,在頭文件中當使用智能指針對象作為類成員變量時,也應該優先使用前置聲明去引用智能指針對象的包裹類,而不是直接包含包裹類的頭文件。
//Test.h
#include?
//智能指針包裹類A,這里優先使用A的前置聲明,而不是直接包含A.h
class?A;
class?Test
{
public:
??Test();
??~Test();
private:??
??std::unique_ptr??m_spA;
};
C++ 新標準中的智能指針我想介紹的就這么多了,Modern C/C++ 已經變為 C/C++ 開發的趨勢,希望讀者能善用和熟練使用本節介紹的后三種智能指針對象。
歷史推薦
1.?聊一聊程序員如何增加收入
2. 這一次我讓你徹底搞明白并發編程
3. 小方說服務器開發——一個實實在在幫你提高后端開發能力的地方
4. 一個 WebSocket 服務器是如何開發出來的?
5.?心跳包設計機制詳解
歡迎關注『高性能服務器開發』公眾號,一起交流服務器編程技藝與思想,也可以加入高性能服務器開發交流群:578019391?一起交流。
如果覺得對您有幫助,點擊“在看”支持下吧!
總結
以上是生活随笔為你收集整理的c++ 智能指针_详解 C++ 11 中的智能指针的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 信工大战略支援部队大四几号毕业
- 下一篇: c++re什么意思_玩转英语词汇--词汇