智能指針之共享指針shared_ptr 的理解、使用(全)

     好長一段時間沒明白共享指針的理解和使用,今天認認真真查了一些資料,搞懂了很多。在這裏整理了一下兩個鏈接的內容。

主要參考鏈接:

       https://blog.csdn.net/u011866460/article/details/42027457

      https://blog.csdn.net/shaosunrise/article/details/85228823

 

       共享指針 (shared_ptr) 是現在的 Boost 庫中提供的,並且應該是將來 C++1x 的標準庫中提供的一個模板類。在此之前,ISO/IEC 14882:2003 標準庫 <memory> 中的“自動指針 (auto_ptr)”也有類似的功能。顯然 shared_ptr 要比 auto_ptr 從功能上來說應該強大一些。這篇文章主要介紹 shared_ptr 的用法及注意事項。

1. shared_ptr 的功能

        shared_ptr 主要的功能是,管理動態創建的對象的銷燬。它的基本原理就是記錄對象被引用的次數,當引用次數爲 0 的時候,也就是最後一個指向某對象的共享指針析構的時候,共享指針的析構函數就把指向的內存區域釋放掉。

        與普通指針相比,共享指針對象重載了 * 、-> 和==運算符, 所以你可以像通常的指針一樣使用它。沒有重載+、-、++、--、[ ]等運算法。

2. shared_ptr 的原理

      它遵循共享所有權的概念,即不同的 shared_ptr 對象可以與相同的指針相關聯,並在內部使用引用計數機制來實現這一點。
每個 shared_ptr 對象在內部指向兩個內存位置:
      1、指向對象的指針。
      2、用於控制引用計數數據的指針。(這裏的引用計數爲指向該內存塊的共享指針個數
共享所有權如何在參考計數的幫助下工作:
     1、當新的 shared_ptr 對象與原共享指針關聯時,則在其構造函數中,將與此指針關聯的引用計數增加1。
     2、當任何 與之相關的shared_ptr 對象被析構時,例如局部函數調用結束,則在其析構函數中,它將關聯指針的引用計數減1。如果引用計數變爲0,則表示沒有其他 shared_ptr 對象與此內存關聯,在這種情況下,它使用delete函數刪除堆中對應內存。

3. shared_ptr 所在庫

3.1. 對於 Visual C++ 2010

     目前,Visual C++ 2010 的 <memory> 庫裏,已經包含了 shared_ptr 模板類,也就是說,你可以直接這樣寫:

      #include <memory>

3.2. 對於其它支持 ISO/IEC 14882:2003 標準的編譯器

       而 GNU G++ 的標準庫中還沒有支持(畢竟是將來的標準),如果在 G++ 中想使用 shared_ptr, 還是得用到 Boost 庫,就是說,在 G++ 裏,你得這樣寫:

     #include <boost/shared_ptr.hpp>

4. shared_ptr 對象的構造

       保險起見,你應該僅從以下幾種途徑構造一個共享指針(以下例子中若沒特殊說明,T 就代表共享指針所指向的對象的類型):

4.1. 使用空參數構造函數構造

      也就是說,你可以直接定義一個 shared_ptr 而不指定構造函數的內容:

       1

std::shared_ptr<T> ptr;

      這樣做的話,ptr 的意義就相當於一個 NULL 指針。當你試圖在一個空指針上做類似於 *ptr 或者 ptr->xx 之類的東西的時候,應該會收到異常的。

4.2. 直接從 new 操作符的返回值構造

      用代碼來表示,就是可以這樣使用:

      1

std::shared_ptr<T> ptr(new T());

       上面這行代碼在堆裏創建了兩塊內存:1.存儲T。2.用於引用計數的內存,管理附加此內存的shared_ptr對象的計數,最初計數將爲1.

       因爲帶有參數的shared_ptr的複製構造函數(4.3)是explicit類型的,所以不能像這樣 std::shared_ptr<T> ptr = new T();隱式調用它構造函數。

       但是鏈接裏說構造新的shared_ptr對象的最佳方法是使用std::make_shared類模板。因爲 1)它一次性爲T對象和用於引用計數的數據都分配了內存,而new操作符只是爲T分配了內存(沒理解,求高人解答)。2)它可以避免一些由堆指針或者new分配指針導致的錯誤。

    1

      std::shared_ptr<T> p1=std::make_shared<T> ();

4.3. 使用複製構造函數(或等號重載),從其它 shared_ptr 的對象構造

       一種顯然的情況是這樣的:

   1
   2

std::shared_ptr<T> ptr1(new T()); // 本行與 3.1. 中的構造方法是一樣的,引用計數爲1
std::shared_ptr<T> ptr2(ptr1);    // 這就是使用複製構造函數的方法,會讓引用計數加 1

       還有,shared_ptr 可以當作函數的參數傳遞,或者當作函數的返回值返回,這個時候其實也相當於使用複製構造函數。

       過程如下:作函數實參時,將指針執行復制構造函數傳入函數體內,因此該內存塊的引用計數+1;當作爲函數返回值時,複製構造函數將內存地址傳遞給新指針,引用計數+1,然後,局部指針執行析構,引用計數-1。

4.4. 從 shared_ptr 提供的類型轉換 (cast) 函數的返回值構造

        shared_ptr 也可以類型轉換,有關類型轉換的詳情參見下面的 6. 此處假設 B 是 A 的子類,那麼,根據C++繼承與派生中的知識,B 的指針當然是可以轉換成 A 的指針的。在共享指針裏,應該這樣做:

  1
  2

std::shared_ptr<B> ptrb(new B());
std::shared_ptr<A> ptra( dynamic_pointer_cast<A>(ptrb) );  //本質還是複製構造

 

5. shared_ptr 的“賦值”

       shared_ptr 也可以直接賦值,但是必須是賦給相同類型的 shared_ptr 對象,而不能是普通的 C 指針或 new 運算符的返回值。當共享指針 a 被賦值成 b 的時候,如果 a 原來是 NULL, 那麼直接讓 a 等於 b 並且讓它們指向的東西的引用計數加 1; 如果 a 原來也指向某些東西的時候,如果 a 被賦值成 b, 那麼原來 a 指向的東西的引用計數被減 1, 而新指向的對象的引用計數加 1. 就是說以下代碼是允許的:

   1
   2
   3

std::shared_ptr<T> a(new T());
std::shared_ptr<T> b(new T());
a = b; // 此後 a 原先所指的對象會被銷燬,b 所指的對象引用計數加 1

shared_ptr 的對象在構造之後,可以被賦予空值,此時使用的應該是 reset() 函數或者nullptr,如:

  1
  2

  3

std::shared_ptr<T> a(new T());
a.reset();         // 此後 a 原先所指的對象的引用計數-1,並且 a 會變成 NULL。這裏內存會被銷燬

a=nullptr;        //同上。推薦多用

當然理論上也可以這樣寫:

  1
  2

std::shared_ptr<T> a(new T());
a = std::shared_ptr<T>(); // ,相當於給 a 賦值一個新構造的 無名shared_ptr對象, 也就是 NULL

6. shared_ptr 的類型轉換

       shared_ptr 有兩種類型轉換的函數,一個是 static_pointer_cast, 一個是 dynamic_pointer_cast. 其實用法真的和 C++ 提供的 static_cast 和 dynamic_cast 很像,再結合 4.4. 的代碼和以下類似的代碼,幾乎沒什麼好講的:

  1
  2
  3

std::shared_ptr<A> ptra;
std::shared_ptr<B> ptrb(new B());
ptra = dynamic_pointer_cast<A>(ptrb);

7. 從 shared_ptr 的對象獲得傳統 C 指針

    很簡單,可以這樣用:

    1
    2

std::shared_ptr<T> ptr(new T());
T *p = ptr.get(); // 獲得傳統 C 指針

     建議少用get()函數,因爲如果在shared_ptr析構之前手動調用了delete函數,同樣會導致類似的錯誤。

8. shared_ptr 的常見的其它用法(重要)

8.1 共享指針的重置

比如,“我想讓一個已經構造好的共享指針,丟棄掉原來所指的對象(或者讓其引用計數減 1),然後指向一個新的 new 出來的對象,該怎麼辦?”參考如下代碼:

     1
      2

std::shared_ptr<T> ptr(new T());
ptr.reset(new T()); // 原來所指的對象會被銷燬

8.2 NULL檢測
        當我們創建 shared_ptr 對象而不分配任何值時,它就是空的,即地址爲000000000;普通指針不分配空間的時候相當於一個野指針,指向垃圾空間,且無法判斷指向的是否是有用數據。
       shared_ptr 檢測空值方法

    1
    2

    3 

    4  

    5 

    6 

    7   

    8 

    9

std::shared_ptr<T> ptr3;
if(!ptr3)
    std::cout<<"Yes, ptr3 is empty" << std::endl;
if(ptr3 == NULL)
    std::cout<<"ptr3 is empty" << std::endl;
if(ptr3 == nullptr)
    std::cout<<"ptr3 is empty" << std::endl;

if(ptr3==0)

    std::cout<<"ptr3 is empty" << std::endl;

 

9. shared_ptr 的錯誤用法

      一定要注意,本節所述所有方法,都是錯誤的!

9.1. 在“中途”使用傳統 C 指針構造共享指針

      所謂在中途,指的就是不從 new 的返回值直接構造共享指針,比如從 this 指針構造自己的共享指針等。

9.2. 從一個對象的傳統 C 指針,構造出兩個或以上的共享指針

     其實這種和 9.1. 也是類似的,或者說,這種情況是 9.1. 的一種具體情況,比如,下面的代碼是錯誤的:

   1
   2
   3

T *a = new T();
shared_ptr<T> ptr1(a);
shared_ptr<T> ptr2(a);

    這樣的話,ptr1 和 ptr2 的引用計數是單獨算的,它們任意一個對象在析構的時候,都會銷燬 a 所指的對象,a就爲懸空指針,所以,這個對象會被“銷燬兩次”。因此報錯。(make_shared類模板可以避免)

9.3 不要用棧中的指針構造shared_ptr對象

     shared_ptr默認的構造函數中使用的是delete來刪除關聯的內存,所以構造的時候也必須使用new出來的堆空間的指針。如果是棧中的指針,當shared_ptr對象超出作用域調用析構函數delete內存時,會報錯。(make_shared類模板可以避免)

10. shared_ptr 的侷限

     有關運行效率的問題在這裏就不討論了。其它方面,shared_ptr 的構造要求比較高,如果對象在創建的時候沒有使用共享指針存儲的話,之後也不能用共享指針管理這個對象了。如果有引用循環 (reference cycle), 也就是對象 a 有指向對象 b 的共享指針,對象 b 也有指向對象 a 的共享指針,那麼它們都不會被析構。當然的,shared_ptr 也沒有辦法和 Garbage Collecting 比較,畢竟如果運行庫能夠干預,還是有算法可以檢查到引用循環的。(例如求強連通分量的算法。)

     尤其,在類的成員函數的編寫的時候,有時我們經常希望得到“自己”的共享指針,但是這往往是無法得到的。此時也不能夠從 this 指針構造自己的共享指針(參見 9.1.),所以有時很憋悶。

11. 總結

     實際上上面這麼多注意事項,中心思想就是一個:讓 shared_ptr 正確地記錄對象被引用次數。如果能悟出一點 shared_ptr 的工作原理,基本上不會弄出太危險的事情來。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章