shared_ptr的一切(本質、make_shared)


參考

(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.明顯的好處

  1. 以前沒有shared_ptr的時候需要,手動 delete,這樣帶來三個明顯問題:

    1. 可能存在忘記delete,造成內存泄漏
    2. 可能存在重復delete,造成重復釋放
  2. 在new 和 手動delete中間,可能存在異常拋出,這樣也會是內存泄漏的原因

  3. 有個shared_ptr,就可以在對象當前作用域結束后,自動 釋放 內部管理的空間(RAII技術)

  4. 當只有一個shared_ptr指向這片內存,在他析構的時候就會自動把這片內存釋放,這是線程安全的釋放!!!!,內部是原子的對引用計數的增加和減少,因而也影響了性能

3. 常用使用方式

  1. 頭文件

    #include <memory>
    
  2. 常用使用方式

    #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出來的這種方式

  3. 使用的方式就和指針一樣

    test->xx函數

    等同於Test的指針->xx函數

  4. 可以使用 *test,得到原始指針的引用值

  5. test.get()得到內部裸漏的指針,多用於兼容其他 c版本的函數

  6. 默認的可以直接 shared_ptr test創建對象

    shared_ptr<Test> test;
    test.reset(new Test());
    

    先把對象創建之后再把管理的對象 移入

  7. reset函數

    減少一個引用計數

  8. use_count函數

    查看一個對象的引用計數

4. 本質

  1. 當use_count 為1的時候的析構被調用 就會析構 _M_ptr否則use_count減一

  2. 那么__shared_count這個指針什么時候析構呢?

    當weak_count為1的時候的析構被調用 就會析構 _M_ptr

    否則weak_count–

  3. 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

  1. 構建一個shared_ptr需要兩次不連續內存分配

    顯示new 來 創建需要管理的內存,比如上面的new Test()

    構建 shared_ptr 然后把 需要管理的內存傳進來,shared_ptr堆上動態創建use_count

    帶來的就是兩次 不連續的 內存創建

  2. 那么 make_shared呢只需要一次連續的分配,shared_ptr內部計數和指向的內存在連續的塊

  3. 借用 http://bitdewy.github.io/blog/2014/01/12/why-make-shared/的圖

  4. 那么 使用make_shared的好處有哪些

    1. 效率更好,因為只需要一次內存分配,並且不需要用戶顯示new
    2. 異常安全
    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發生異常,內存泄漏就出來了

    解決辦法:

    1. 獨立的語句

      shared_ptr a = shared_ptr (new Test());

      f(a, getMemory());

    2. make_shared

      f(make_shared (), getMemory());

  5. make_shared的壞處

    1. 需要保證構造函數c++ - How do I call ::std::make_shared on a class with only protected or private constructors? - Stack Overflow

    2. 可能造成部分內存鎖定

      因為上面的分析我們知道 weak_count管理哪些引用的個數,當這個為0釋放那個count結構

      但是make_shared 將管理的對象內存和count結構的內存綁定在一起,盡管use_count計數為0釋放了空間

      由於count結構可能存在,只有當 weak_count釋放count結構的時候,這整個的由make_shared 釋放的空間才能歸還

  6. 題外

    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,但是這其實是不准確的

​ 看圖:

繪圖1

  1. Control Block是從堆內存創建的空間,所以自定義析構器不會影響shared_ptr本身的大小
  2. Control Block里面為啥還有指針指向 T Object請看 問題4 為啥__shared_count里面還有指向ptr的指針
  3. 該Control Block在裸指針創建shared_ptr或者unique_ptr移動構造或weak_ptr構造才會被創建,這也暗示我們用相同的裸指針初始化兩個shared_ptr就會創建兩個Control Block導致重復釋放

8. 合理使用enable_shared_from_this

​ enable_shared_from_this,這個是一個類用於在函數內部將this指針封裝為shared_ptr返回

  1. 為啥需要這個我直接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啊,最終導致重復釋放了啊啊啊

    1. 怎么使用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 派生類和基本賦值問題

  1. 默認的指針隱式轉換 到了 模板中該怎么辦?

    比如 子類指針可以直接賦值給 父類指針

    子類對象可以直接賦值給 父類對象

    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;
            
    };
    
    變成上面這樣就解決了
    
    1. 不加 explicit的原因是 原生的就支持 隱式轉換

    2. 並且 m_ptr(user.m_ptr),編譯器會判斷這兩個內部指針能否進行賦值,這也就是實現了 相關的類賦值以及 子類賦值給父類的強制要求

      只有在 U指針可以隱式轉換為 T指針才能被編譯

    3. 另一個成員函數模板的用處: 賦值操作

    比如標准 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進行隱式拷貝構造  
    };
    
    1. 只有泛化構造是 explicit的,所以 只有這個是隱式轉換
    2. 對於 auto_ptr 本來就改動了,不需要 const
  2. 當泛化構造和 默認的拷貝構造 ?

    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內部的指針值也可能不同


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM