參考
(2條消息) 為什么多線程讀寫 shared_ptr 要加鎖?_陳碩的Blog-CSDN博客
(2條消息) C++11使用make_shared的優勢和劣勢_yagerfgcs的博客-CSDN博客_makeshared
(2條消息) C++11新特性之十:enable_shared_from_this_草上爬的博客-CSDN博客
第22課 weak_ptr弱引用智能指針 - 淺墨濃香 - 博客園 (cnblogs.com)
(2條消息) C++:智能指針(5)——enable_shared_from_this工作原理、源碼分析_cocoa0409的博客-CSDN博客
(2條消息) C++11新特性之十:enable_shared_from_this_草上爬的博客-CSDN博客
1.作用
作為可以共享的智能指針來管理堆內存,當最后一個智能指針進行析構的時候,內部引用計數歸零,也就是沒有智能指向指向這片堆內存,進行內存釋放
最大的好處是在對象不被需要的時候進行釋放
2.明顯的好處
-
以前沒有shared_ptr的時候需要,手動 delete,這樣帶來三個明顯問題:
- 可能存在忘記delete,造成內存泄漏
- 可能存在重復delete,造成重復釋放
-
在new 和 手動delete中間,可能存在異常拋出,這樣也會是內存泄漏的原因
-
有個shared_ptr,就可以在對象當前作用域結束后,自動 釋放 內部管理的空間(RAII技術)
-
當只有一個shared_ptr指向這片內存,在他析構的時候就會自動把這片內存釋放,這是線程安全的釋放!!!!,內部是原子的對引用計數的增加和減少,因而也影響了性能
3. 常用使用方式
-
頭文件
#include <memory>
-
常用使用方式
#include <iostream> #include <memory> using namespace std; class Test{ public: Test() { std::cout << "Test()" << std::endl; } ~Test() { std::cout << "~Test()" << std::endl; } }; int main() { shared_ptr<Test> test(new Test()); shared_ptr<Test> test(make_shared<Test>());//推薦這種 return 0; }
輸出:
Test() ~Test()
在main的這個{}作用域內結束后,自動釋放Test空間
必須用explicit構建,因為內部構造函數使用explicit的方式
都是使用智能指針類值的方式管理 其他內存,一般不會使用智能指針還是 new出來的這種方式
-
使用的方式就和指針一樣
test->xx函數
等同於Test的指針->xx函數
-
可以使用 *test,得到原始指針的引用值
-
test.get()得到內部裸漏的指針,多用於兼容其他 c版本的函數
-
默認的可以直接 shared_ptr
test創建對象 shared_ptr<Test> test; test.reset(new Test());
先把對象創建之后再把管理的對象 移入
-
reset函數
減少一個引用計數
-
use_count函數
查看一個對象的引用計數
4. 本質
-
當use_count 為1的時候的析構被調用 就會析構 _M_ptr否則use_count減一
-
那么__shared_count這個指針什么時候析構呢?
當weak_count為1的時候的析構被調用 就會析構 _M_ptr
否則weak_count–
-
shared_ptr中自定義析構器不會影響大小,這和unique_ptr不同,這里面的圖會發現,一個shared_ptr包含好多東西哇
動態分布的Control Block
任意大的刪除器和分配器
虛函數Function
原子的use_count和weak_count
5. 注意點
1. shared_ptr可以由unique_ptr創建,但是絕不可以unique_ptr由shared_ptr創建,因為shared_ptr內部的use_count即使為1也不會因為賦值給unique_ptr改變的
2. shared_ptr僅僅只針對單一的對象,他和unique_ptr不同,沒有shared_ptr<T[]>, 也不應該有,因為shared_ptr允許子類賦值父類,參見 **問題3 :shared_ptr 派生類和基本賦值問題**,當出現數組那么就非常不正確了;因而因為這一點在 unique_ptr<T[]>中禁用了這種賦值
5. shared_ptr Vs make_shared
-
構建一個shared_ptr需要兩次不連續內存分配
顯示new 來 創建需要管理的內存,比如上面的new Test()
構建 shared_ptr 然后把 需要管理的內存傳進來,shared_ptr堆上動態創建use_count
帶來的就是兩次 不連續的 內存創建
-
那么 make_shared呢只需要一次連續的分配,shared_ptr內部計數和指向的內存在連續的塊
-
借用 http://bitdewy.github.io/blog/2014/01/12/why-make-shared/的圖
-
那么 使用make_shared的好處有哪些
- 效率更好,因為只需要一次內存分配,並且不需要用戶顯示new
- 異常安全
f(shared_ptr<Test>(new Test()), getMemory());
我們以為的順序:
1. new Test 2. std::shared_ptr 3. getMemory()
不同的語言對於函數參數的順序是不確定的, 但是 new Test肯定在 std::shared_ptr之前
那么
1. new Test 2. getMemory 3. std::shared_ptr
當getMemory發生異常,內存泄漏就出來了
解決辦法:
-
獨立的語句
shared_ptr
a = shared_ptr (new Test()); f(a, getMemory());
-
make_shared
f(make_shared
(), getMemory());
-
make_shared的壞處
-
可能造成部分內存鎖定
因為上面的分析我們知道 weak_count管理哪些引用的個數,當這個為0釋放那個count結構
但是make_shared 將管理的對象內存和count結構的內存綁定在一起,盡管use_count計數為0釋放了空間
由於count結構可能存在,只有當 weak_count釋放count結構的時候,這整個的由make_shared 釋放的空間才能歸還
-
題外
Foo 的構造函數參數要傳給 make_shared(),后者再傳給 Foo::Foo(),這只有在 C++11 里通過 perfect forwarding 才能完美解決
6. 合適的時機使用移動構造shared_ptr
我們需要知道當我們每次創建同一shared_ptr的拷貝,帶來的是引用計數的增加並且還是原子的,那么使用移動構造后,直接把old的內部全都轉移到新的shared_ptr里面,這樣就不需要原來的那些開銷
7. 有趣的事情 : shared_ptr中自定義析構器不會影響大小,這和unique_ptr不同
其實這個話題需要知道shared_ptr內部的結構,我們平常總說 shared_ptr內部有一個裸指針 + 引用計數count,但是這其實是不准確的
看圖:
- Control Block是從堆內存創建的空間,所以自定義析構器不會影響shared_ptr本身的大小
- Control Block里面為啥還有指針指向 T Object請看 問題4 為啥__shared_count里面還有指向ptr的指針
- 該Control Block在裸指針創建shared_ptr或者unique_ptr移動構造或weak_ptr構造才會被創建,這也暗示我們用相同的裸指針初始化兩個shared_ptr就會創建兩個Control Block導致重復釋放
8. 合理使用enable_shared_from_this
enable_shared_from_this,這個是一個類用於在函數內部將this指針封裝為shared_ptr返回
-
為啥需要這個我直接this返回不行么
#include <iostream> // std::streambuf, std::cout #include <memory> // std::ofstream #include <functional> // std::ofstream using namespace std; class Test { public: shared_ptr<Test> get() { return shared_ptr<Test>(this); } ~Test() { std::cout << "~Test()" << std::endl; } }; int main () { shared_ptr<Test> test(make_shared<Test>()); shared_ptr<Test> test1 = test->get(); return 0; }
我想你猜到了,注意我們之前說的那個Control Block, 這里分別使用this裸指針和make_shared來創建shared_ptr,結果就是創建了兩個不同的Control Block啊,最終導致重復釋放了啊啊啊
- 怎么使用enable_shared_from_this
#include <iostream> // std::streambuf, std::cout #include <memory> // std::ofstream #include <functional> // std::ofstream using namespace std; class Test : public enable_shared_from_this<Test>{ public: shared_ptr<Test> get() { return shared_from_this(); } ~Test() { std::cout << "~Test()" << std::endl; } }; int main () { shared_ptr<Test> test(make_shared<Test>()); shared_ptr<Test> test1 = test->get(); return 0; }
需要注意的是,必須Test是由shared_ptr管理的之后才能調用shared_from_this,其實你可能猜到enable_shared_from_this的實現就是依賴於shared_ptr的,shared_from_this不能在構造函數中調用,因為對象還沒有構建(shared_ptr還沒賦值呢)
問題1:當多個線程執行shared_ptr析構是否出現重復釋放?
shared_ptr對管理的內存是線程安全的
因為源碼中
_Atomic_word _M_use_count; // #shared
_Atomic_word _M_weak_count;
析構的時候
if (__gnu_cxx::__exchange_and_add_dispatch(&_M_use_count, -1) == 1)
{
類型都是原子類型,對他們的操作都是采用原子操作來實現了
即使多個線程來減少,他僅僅判斷_M_use_count為1的時候進行 -1才會釋放內存
問題2: 多線程讀寫shared_ptr需要加鎖嗎
多線程引用計數是安全的,但是對象的讀寫不是
**如果要從多個線程讀寫同一個 shared_ptr 對象,那么需要加鎖**
參見[(2條消息) 為什么多線程讀寫 shared_ptr 要加鎖?_陳碩的Blog-CSDN博客](https://blog.csdn.net/Solstice/article/details/8547547)
本質就是 在 內部的指針和 count計數對象賦值這兩個步驟整體不是原子的問題
問題3 :shared_ptr 派生類和基本賦值問題
-
默認的指針隱式轉換 到了 模板中該怎么辦?
比如 子類指針可以直接賦值給 父類指針
子類對象可以直接賦值給 父類對象
class Top { //..... }; class Middle : public Top { //... }; class Bottom : public Top { //... }; int main(int argc, char**argv) { Top* middle = new Middle(); //子類賦值給父類 Top* bottom = new Bottom(); //子類賦值給父類 const Top* top = middle; //父類隱式給const父類 return 0; }
上面肯定沒問題,那么當和智能指針碰撞呢,我們希望有這樣的實現
Shared_ptr<Top> pt1 = Shared_ptr<Middle>(new Middle()); Shared_ptr<Top> pt2 = Shared_ptr<Middle>(new Bottom());
但是!!!,如果不自己處理,默認是不可能的的!!,因為 Shared_ptr
Shared_ptr (new Middle()) 沒有任何關系 解決辦法:
編寫轉換構造函數,這個轉換構造函數的參數肯定不能是某一個固定類型的參數,因為 Top會有很多子類,一個一個寫累死了
template<typename T> class SmartPtr { public: template<typename U> SmartPtr(const SmartPtr<U>& user) : m_ptr(user.m_ptr) { } T* get() const { return m_ptr; } private: T* m_ptr; };
變成上面這樣就解決了
-
不加 explicit的原因是 原生的就支持 隱式轉換
-
並且 m_ptr(user.m_ptr),編譯器會判斷這兩個內部指針能否進行賦值,這也就是實現了 相關的類賦值以及 子類賦值給父類的強制要求
只有在 U指針可以隱式轉換為 T指針才能被編譯
-
另一個成員函數模板的用處: 賦值操作
比如標准 shared_ptr之類的
template<class T> class shared_ptr { public: template<class Y> explicit shared_ptr(Y *p); //構造,可以進行任何和T兼容的指針的賦值 template<class Y> shared_ptr(shared_ptr<Y>const &p); //對shared_ptr進行隱式拷貝構造 template<class Y> explicit shared_ptr(weak_ptr<Y>const &p); //對weak_ptr進行隱式拷貝構造 template<class Y> explicit shared_ptr(auto_ptr<Y>const &p); //對auto_ptr進行隱式拷貝構造 template<class Y> shared_ptr& operator=(shared_ptr<Y>const &p); //對shared_ptr進行賦值拷貝 template<class Y> shared_ptr& operator=(auto_ptr<Y>const &p); //對auto_ptr進行隱式拷貝構造 };
- 只有泛化構造是 explicit的,所以 只有這個是隱式轉換
- 對於 auto_ptr 本來就改動了,不需要 const
-
-
當泛化構造和 默認的拷貝構造 ?
member template並不改變語言規則--》如果你沒有主動實現拷貝構造,編譯器默認給你生成
這二者不沖突,如果你想要控制copy構造的每一個細節,必須同時聲明實現泛化和拷貝構造
問題4 為啥__shared_count里面還有指向ptr的指針
基於問題3,為了精准調用對應的析構函數,即使沒有虛析構的存在下
上面例子中
Shared_ptr<Top> pt1 = Shared_ptr<Middle>(new Middle());
pt1在析構的時候調用的是 delete Middle, 而不是 Top
看源碼:
template<typename _Tp1>
shared_ptr(const shared_ptr<_Tp1>& __r, _Tp* __p) noexcept
: __shared_ptr<_Tp>(__r, __p) { }
然后 __shared_ptr:
template<typename _Tp1, typename = typename
std::enable_if<std::is_convertible<_Tp1*, _Tp*>::value>::type>
__shared_ptr(const __shared_ptr<_Tp1, _Lp>& __r) noexcept
: _M_ptr(__r._M_ptr), _M_refcount(__r._M_refcount)
{ }
_Tp1是參數類型推導,類的類型是 _Tp
調用->的使用使用的類型是 _ Tp(top)類型,在 __shared_ptr參數轉換構造的時候,_M_ptr參數內部包含的是 _Tp1(Middle)類型
這樣之后不管你 shared_ptr指向什么類型,即使是void, 那么析構的時候 Middle仍然被正常析構
當出現虛析構的時候 shared_ptr內部的指針和 __shared_count內部的指針值也可能不同