1 控制塊
shared_ptr 繼承自 _Ptr_base,它包含兩個成員變量:指向目標對象的指針 _Ptr 和 引用計數基類指針 _Rep。
private: element_type * _Ptr{nullptr}; _Ref_count_base * _Rep{nullptr};
引用計數的基類是 _Ref_count_base,在 _Ref_count_base 中,實現了計數的增加和減少,以及對象的釋放。其中_Destroy() 和 _Delete_this() 都是虛函數,它派生出了三個類 _Ref_count,_Ref_count_resource 和 _Ref_count_resource_alloc,這些也被稱為控制塊。
- _Ref_count 只包含一個指向對象的指針,直接使用 delete 進行釋放資源和自身。
- _Ref_count_resource 使用 _Compressed_pair<_Dx, _Resource> _Mypair 存儲刪除器和對象,在 _Destroy()中通過刪除器釋放對象,_Delete_this() 無變化。
- _Ref_count_resource_alloc 增加了分配器,_Destroy()無變化,_Delete_this() 通過分配器釋放自身。
_Compressed_pair 和 pair 差不多,但是進行了一些優化,如果 _Ty1 是空類, _Compressed_pair 就會繼承這個空類,此時 sizeof(_Compressed_pair) == sizeof(_Ty2),這就是所謂的 EBO(empty base optimization;空白基類優化)。
2 shared_ptr 的構造函數
shared_ptr 有多個構造函數,根據指針、刪除器、分配器的不同去生成相應的_Ref_count,_Ref_count_resource 和 _Ref_count_resource_alloc。
以僅需指針的構造函數為例,is_array 判斷是否是數組從而去調用不同的 _Setp 重載函數,在 _Setp 中生成控制塊,然后調用_Set_ptr_rep_and_enable_shared 函數將 shared_ptr 的 _Ptr 和 _Rep 指向相應的地址。
在代碼的最后還調用了_Enable_shared_from_this 函數,這個比較復雜,放到最后介紹。
3 shared_ptr 的引用計數
shared_ptr 的引用計數通過 _Rep 指向的控制塊控制,相關的操作在基類 _Ref_count_base 中已經實現,主要就是當 _Uses 為 0 時,調用 _Destroy() 釋放對象;當 _Weaks 為 0 時,調用 _Delete_this() 釋放自己,其余還有引用計數的增加和減少等函數。
4 make_shared 與 直接創建 shared_ptr 的區別
make_shared 只需要分配一次內存,而直接創建 shared_ptr 需要分配兩次內存。
shared_ptr<string> sp{new string("hello")};
比如上面我們采用直接創建的方式,那么首先需要在堆上為 hello 分配內存,其次根據上面 shared_ptr 的構造函數可知,new _Ref_count<_Ux>(_Px) 時還需要為控制塊分配一次內存。
然后我們再來看看 make_shared 源碼,它動態分配了一個 _Ref_count_obj,這也是 _Ref_count_base 的一個派生類,aligned_union_t<1, _Ty> _Storage 為 _Ty 類型的對象分配了內存空間,所以對象和控制塊都被放在了一塊。
剩下的操作是一樣的,調用_Set_ptr_rep_and_enable_shared 函數將 shared_ptr 的 _Ptr 和 _Rep 指向相應的地址。
借用網上的一張圖,它們的區別大概就是:
當然,make_shared 也存在缺陷,上面我們提到,在_Ref_count_base的實現中,只有當 _Weaks 為 0 時,控制塊才會調用 _Delete_this() 釋放自己,然而由於現在對象也被放在了控制塊中,所以即使 _Uses 為 0 時,對象空間也沒有算被釋放,只有當 _Weaks 為 0 時, 才算是真的被釋放了。
5 enable_shared_from_this
為什么需要 enable_shared_from_this,是因為引用計數和對象是分離的,在某些情況下會造成錯誤。舉一個網上常見的例子:
#include <iostream> #include <memory> using namespace std; class A { public: shared_ptr<A> getPtr() { return shared_ptr<A>(this); } ~A() { cout << "~A()" << endl; } }; int main() { shared_ptr<A> sp1(new A()); shared_ptr<A> sp2 = sp1->getPtr(); cout << "sp1.use_count() = " << sp1.use_count() << endl; cout << "sp2.use_count() = " << sp2.use_count() << endl; }
在上面的代碼中,sp1 和 sp2 指針指向同一個對象,但是它們的引用計數為什么都是 1 呢?因為當你傳一個指針過去的時候,shared_ptr 根本就不知道是否已經有 shared_ptr 指向它,所以它會自己創建控制塊,最后導致同一對象會出現釋放兩次的情況。所以凡是需要共享使用類對象的地方,必須使用這個 shared_ptr 當作右值來構造或者拷貝構造(shared_ptr 類中定義了賦值運算符函數和拷貝構造函數)另一個 shared_ptr ,從而達到共享使用的目的。
解決這個問題的方法是通過 weak_ptr ,我們只需要調用 shared_from_this() 函數,它通過 weak_ptr 來構造一個 shared_ptr。
我們可以做一個簡單的嘗試:
#include <iostream> #include <memory> using namespace std; int main() { shared_ptr<int> sp1(new int(5)); weak_ptr<int> wp(sp1); cout << "sp1.use_count() = " << sp1.use_count() << endl; shared_ptr<int> sp2(wp); cout << "sp2.use_count() = " << sp2.use_count() << endl; shared_ptr<int> sp3(wp); cout << "sp2.use_count() = " << sp3.use_count() << endl; }
現在我們從源碼角度來分析它是如何實現的,我們查看 class enable_shared_from_this 的結構:
其中重點關注 _Ptr->_Wptr = shared_ptr<remove_cv_t<_Yty>>(_This, const_cast<remove_cv_t<_Yty> *>(_Ptr)) 這句代碼,它將調用下述代碼通過 _This 和 _Ptr 生成 shared_ptr。
最后一步是通過 _Weakly_construct_from 將剛才生成的shared_ptr 轉換成 weak_ptr。
最后,當我們使用 shared_from_this() 時,就可以通過我們所保存的 weak_ptr 來生成 shared_ptr。
現在,我們可以通過 enable_shared_from_this 來修改最初的程序:
#include <iostream> #include <memory> using namespace std; class A: public std::enable_shared_from_this<A> { public: shared_ptr<A> getPtr() { return shared_from_this(); } ~A() { cout << "~A()" << endl; } }; int main() { shared_ptr<A> sp1(new A()); shared_ptr<A> sp2 = sp1->getPtr(); cout << "sp1.use_count() = " << sp1.use_count() << endl; cout << "sp2.use_count() = " << sp2.use_count() << endl; }
References: