C++11 shared_ptr 與 make_shared源碼剖析詳解
0. 前言
所謂智能指針,可以從字面上理解為“智能”的指針。具體來講,智能指針和普通指針的用法是相似的,不同之處在于,智能指針可以在適當時機自動釋放分配的內(nèi)存。也就是說,使用智能指針可以很好地避免“忘記釋放內(nèi)存而導致內(nèi)存泄漏”問題出現(xiàn)。由此可見,C++ 也逐漸開始支持垃圾回收機制了,盡管目前支持程度還有限。
c++11 中發(fā)布了shared_ptr
、unique_ptr
、weak_ptr
用以資源的管理,都是定義在memory 這個頭文件中。
std::shared_ptr
允許多個shared_ptr 實例指向同一個對象,通過計數(shù)管理;std::unique_ptr
是獨占對象;weak_ptr
是輔助類,是一種弱引用,指向shared_ptr 所管理的對象。
1. 源碼分析
1.1 頭文件
#include <memory>
1.2 構造
constexpr shared_ptr() noexcept; template<class Y> explicit shared_ptr(Y* p); template<class Y, class D> shared_ptr(Y* p, D d); template<class Y, class D, class A> shared_ptr(Y* p, D d, A a); template <class D> shared_ptr(nullptr_t p, D d); template <class D, class A> shared_ptr(nullptr_t p, D d, A a); template<class Y> shared_ptr(const shared_ptr<Y>& r, T *p) noexcept; shared_ptr(const shared_ptr& r) noexcept; template<class Y> shared_ptr(const shared_ptr<Y>& r) noexcept; shared_ptr(shared_ptr&& r) noexcept; template<class Y> shared_ptr(shared_ptr<Y>&& r) noexcept; template<class Y> explicit shared_ptr(const weak_ptr<Y>& r); template<class Y> shared_ptr(auto_ptr<Y>&& r); // removed in C++17 template <class Y, class D> shared_ptr(unique_ptr<Y, D>&& r); shared_ptr(nullptr_t) : shared_ptr() { }
構造函數(shù)比較多啊,抽一個看看源碼。
1.2.1 shared_ptr 的移動構造函數(shù)
template<class _Tp> inline shared_ptr<_Tp>::shared_ptr(shared_ptr&& __r) _NOEXCEPT : __ptr_(__r.__ptr_), __cntrl_(__r.__cntrl_) { __r.__ptr_ = 0; __r.__cntrl_ = 0; }
大概知道,shared_ptr 中存放一個對象的指針__ptr_ 和用以計數(shù)的__cntrl_,這兩是shared_ptr 的私有成員變量:
template<class _Tp> class shared_ptr { typedef _Tp element_type; private: element_type* __ptr_; __shared_weak_count* __cntrl_; ... }
另外,移動構造函數(shù)因為只是move,所以只是將舊的shared_ptr 轉移到新的里。
1.2.2 shared_ptr 的拷貝構造函數(shù)
template<class _Tp> inline shared_ptr<_Tp>::shared_ptr(const shared_ptr& __r) _NOEXCEPT : __ptr_(__r.__ptr_), __cntrl_(__r.__cntrl_) { if (__cntrl_) __cntrl_->__add_shared(); }
與移動構造相同,shared_ptr 實例,需要從參數(shù)中獲得__ptr_ 和 __cntrl。
但是,與移動構造函數(shù)不同的是,拷貝構造時增加對象計數(shù)的。
下面舉例shared_ptr 通常的創(chuàng)建方式:
std::shared_ptr<int> p1; //不傳入任何實參 std::shared_ptr<int> p2(nullptr); //傳入空指針 nullptr std::shared_ptr<int> p3(new int(10)); //指定指針為參數(shù) std::shared_ptr<int> p4(p3); //或者 std::shared_ptr<int> p4 = p3; std::shared_ptr<int> p5(std::move(p4)); //或者 std::shared_ptr<int> p5 = std::move(p4);
1.3 賦值重載
shared_ptr& operator=(const shared_ptr& r) noexcept; template<class Y> shared_ptr& operator=(const shared_ptr<Y>& r) noexcept; shared_ptr& operator=(shared_ptr&& r) noexcept; template<class Y> shared_ptr& operator=(shared_ptr<Y>&& r); template<class Y> shared_ptr& operator=(auto_ptr<Y>&& r); // removed in C++17 template <class Y, class D> shared_ptr& operator=(unique_ptr<Y, D>&& r);
1.4 修改的接口
void swap(shared_ptr& r) noexcept; void reset() noexcept; template<class Y> void reset(Y* p); template<class Y, class D> void reset(Y* p, D d); template<class Y, class D, class A> void reset(Y* p, D d, A a);
reset 基本上是對應構造
1.5 獲取
T* get() const noexcept; T& operator*() const noexcept; T* operator->() const noexcept; long use_count() const noexcept; bool unique() const noexcept; explicit operator bool() const noexcept;
對于shared_ptr 的成員函數(shù)總結如下:
成員方法名 |
功 能 |
operator=() |
重載賦值號,使得同一類型的 shared_ptr 智能指針可以相互賦值。 |
operator*() |
重載 * 號,獲取當前 shared_ptr 智能指針對象指向的數(shù)據(jù)。 |
operator->() |
重載 -> 號,當智能指針指向的數(shù)據(jù)類型為自定義的結構體時,通過 -> 運算符可以獲取其內(nèi)部的指定成員。 |
swap() |
交換 2 個相同類型 shared_ptr 智能指針的內(nèi)容。 |
reset() |
當函數(shù)沒有實參時,該函數(shù)會使當前 shared_ptr 所指堆內(nèi)存的引用計數(shù)減 1,同時將當前對象重置為一個空指針;當為函數(shù)傳遞一個新申請的堆內(nèi)存時,則調(diào)用該函數(shù)的 shared_ptr 對象會獲得該存儲空間的所有權,并且引用計數(shù)的初始值為 1。 |
get() |
獲得 shared_ptr 對象內(nèi)部包含的普通指針。 |
use_count() |
返回同當前 shared_ptr 對象(包括它)指向相同的所有 shared_ptr 對象的數(shù)量。 |
unique() |
判斷當前 shared_ptr 對象指向的堆內(nèi)存,是否不再有其它 shared_ptr 對象再指向它。 |
operator bool() |
判斷當前 shared_ptr 對象是否為空智能指針,如果是空指針,返回 false;反之,返回 true。 |
除了上面的成員函數(shù)外,C++11 標準還支持同一類型的 shared_ptr 對象,或者 shared_ptr 和 nullptr 之間,進行 ==,!=,,>= 運算。
注意:
- shared_ptr用以共享一個對象的所有權,通過計數(shù)確認自動回收;
- shared_ptr 共享的對象所有權,通過存儲對象的指針,并通過get() 獲取存儲的對象的指針;
- 可能在多個線程中同時用不同的shared_ptr實例,而這些實例可能指向同一個對象。多線程在沒有同步的情況訪問同一個shared_ptr 實例,并調(diào)用其non-const 成員函數(shù)時,有可能存在數(shù)據(jù)競爭??梢允褂胹td::atomic* 函數(shù)保護數(shù)據(jù)競爭;
2. make_shared
template<class T, class... Args> shared_ptr<T> make_shared(Args&&... args);
c++11 中針對shared_ptr 還提供了make_shared 這個外部函數(shù),用以創(chuàng)建一個shared_ptr 實例。
c++ 建議盡可能使用make_shared 創(chuàng)建shared_ptr 實例。
下面來說明下make_shared 的優(yōu)缺點。
2.1 make_shared 優(yōu)點
2.1.1 效率高
shared_ptr 需要維護引用計數(shù)的信息:
- 強引用, 用來記錄當前有多少個存活的 shared_ptrs 正持有該對象. 共享的對象會在最后一個強引用離開的時候銷毀( 也可能釋放).
- 弱引用, 用來記錄當前有多少個正在觀察該對象的 weak_ptrs. 當最后一個弱引用離開的時候, 共享的內(nèi)部信息控制塊會被銷毀和釋放 (共享的對象也會被釋放, 如果還沒有釋放的話).
如果你通過使用原始的 new 表達式分配對象, 然后傳遞給 shared_ptr (也就是使用 shared_ptr 的構造函數(shù)) 的話, shared_ptr 的實現(xiàn)沒有辦法選擇, 而只能單獨的分配控制塊:
auto p = new widget(); shared_ptr sp1{ p }, sp2{ sp1 };
如果選擇使用 make_shared 的話, 情況就會變成下面這樣:
auto sp1 = make_shared(), sp2{ sp1 };
內(nèi)存分配的動作, 可以一次性完成. 這減少了內(nèi)存分配的次數(shù), 而內(nèi)存分配是代價很高的操作。
2.1.2 異常安全
void F(const std::shared_ptr<Lhs>& lhs, const std::shared_ptr<Rhs>& rhs) { /* ... */ } F(std::shared_ptr<Lhs>(new Lhs("foo")), std::shared_ptr<Rhs>(new Rhs("bar")));
C++ 是不保證參數(shù)求值順序, 以及內(nèi)部表達式的求值順序的, 所以可能的執(zhí)行順序如下:
- new Lhs(“foo”))
- new Rhs(“bar”))
- std::shared_ptr
- std::shared_ptr
好了, 現(xiàn)在我們假設在第 2 步的時候, 拋出了一個異常 (比如 out of memory, 總之, Rhs 的構造函數(shù)異常了), 那么第一步申請的 Lhs 對象內(nèi)存泄露了. 這個問題的核心在于, shared_ptr 沒有立即獲得裸指針
我們可以用如下方式來修復這個問題.
auto lhs = std::shared_ptr<Lhs>(new Lhs("foo")); auto rhs = std::shared_ptr<Rhs>(new Rhs("bar")); F(lhs, rhs);
當然, 推薦的做法是使用 std::make_shared 來代替:
F(std::make_shared<Lhs>("foo"), std::make_shared<Rhs>("bar"));
2.2 make_shared缺點
構造函數(shù)是保護或私有時,無法使用 make_shared
make_shared 雖好, 但也存在一些問題, 比如, 當我想要創(chuàng)建的對象沒有公有的構造函數(shù)時, make_shared 就無法使用了, 當然我們可以使用一些小技巧來解決這個問題, 比如這里 How do I call ::std::make_shared on a class with only protected or private constructors?
對象的內(nèi)存可能無法及時回收
make_shared 只分配一次內(nèi)存, 這看起來很好. 減少了內(nèi)存分配的開銷. 問題來了, weak_ptr 會保持控制塊(強引用, 以及弱引用的信息)的生命周期, 而因此連帶著保持了對象分配的內(nèi)存, 只有最后一個 weak_ptr 離開作用域時, 內(nèi)存才會被釋放. 原本強引用減為 0 時就可以釋放的內(nèi)存, 現(xiàn)在變?yōu)榱藦娨? 若引用都減為 0 時才能釋放, 意外的延遲了內(nèi)存釋放的時間. 這對于內(nèi)存要求高的場景來說, 是一個需要注意的問題
無法像shared_ptr 的構造那樣添加一個deleter
3. 舉例
#include <iostream> #include <memory> using namespace std; int main() { //構建 2 個智能指針 std::shared_ptr<int> p1(new int(10)); std::shared_ptr<int> p2(p1); //輸出 p2 指向的數(shù)據(jù) cout << *p2 << endl; p1.reset();//引用計數(shù)減 1,p1為空指針 if (p1) { cout << "p1 不為空" << endl; } else { cout << "p1 為空" << endl; } //以上操作,并不會影響 p2 cout << *p2 << endl; //判斷當前和 p2 同指向的智能指針有多少個 cout << p2.use_count() << endl; return 0; }
運行結果:
10
p1 為空
10
1
參考:
C++11 std::shared_ptr總結與使用示例代碼詳解
總結
本篇文章就到這里了,希望能夠給你帶來幫助,也希望您能夠多多關注腳本之家的更多內(nèi)容!
相關文章
Visual Studio 如何創(chuàng)建C/C++項目問題
這篇文章主要介紹了Visual Studio 如何創(chuàng)建C/C++項目問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-02-02C++中priority_queue與仿函數(shù)實現(xiàn)方法
這篇文章主要給大家介紹了關于C++中priority_queue與仿函數(shù)實現(xiàn)的相關資料,優(yōu)先級隊列是一種容器適配器,其底層通常采用vector容器,并通過堆算法來維護元素的順序,文中通過代碼介紹的非常詳細《》需要的朋友可以參考下2024-10-10基于C++內(nèi)存分配、函數(shù)調(diào)用與返回值的深入分析
本篇文章是對C++中的內(nèi)存分配、函數(shù)調(diào)用與返回值進行了詳細的分析介紹,需要的朋友參考下2013-05-05