c++ shared_ptr

  • shared_ptr是通過指針保持對象共享所有權的智能指針。多個shared_ptr對象可佔有同一資源,當最後一個shared_ptr對象被銷燬或者通過operator=,reset()操作賦予另一指針時,其管理的資源纔會被回收。
  • 管理同一資源的不同shared_ptr對象能在不同線程中不加同步的調用其所有成員函數。當然這裏指的是shared_ptr對象本身的成員函數,如果你想多線程訪問其管理的資源,那麼並不會有這種保證。
  • 其成員類型、成員函數與成員變量等在標準中十分明確,在此不再贅述:https://en.cppreference.com/w...
  • shared_ptr也可以指定刪除器,但與unique_ptr不同的是,該刪除器類型並不作爲shared_ptr模板中的參數之一。
  • C++17之前,shared_ptr管理動態分配的數組需要提供自定義的刪除器。c++17可以管理動態數組,例如shared_ptr<int[]> sp(new int[10])。爲了支持這一點,element_type現在被定義爲remove_extent_t<T>。
  • 話不多說,我們來看它的源代碼實現:

25.png

  • 與unique_ptr不同,shared_ptr並未對管理數組對象特化一個版本。

26.png

  • 這個類沒有成員變量,其數據存儲於基類_Ptr_base<_Ty>中,在其成員函數中絕大部分操作也是調用基類提供的函數完成。
  • 接下來我們分析一下_Ptr_base基類的實現。_Ptr_base是一個模板類,模板參數是shared_ptr管理的指針對應的類型。該基類擁有如下這兩個數據成員:

36.png

  • 關於element_type :

37.png

38.png

  • 可以看到element_type 就是模板參數對應的標量類型(scalar type)(since c++17)。
  • 關於_Ref_count_base:

39.png

  • 從名字上就可以看出來,這是一個引用計數的基類,其有兩個純虛函數_Destroy()和_Delete_this(),並且擁有兩個數據成員_Uses和_Weaks,其類型都爲_Atomic_counter_t,shared_ptr之所以在多線程條件下可以不加同步的訪問,就是因爲其內部的引用計數的變化都是用原子操作實現的。這個基類還有對應的對_Uses和_Weaks操作的函數,這些函數保證了在多線程條件下修改引用計數的原子性,我們放到最後分析。
  • 繼承自該類的有五個類,我們研究前三個(後兩個派生類目前沒有發現是幹嘛用的。。):

40.png

  • 從模板參數可以看出來,這三個類分別代表了採用默認刪除器和默認內存分配器的版本、採用自定義刪除器和默認內存分配器的版本和採用自定義的刪除器和內存分配器的版本。我們先來分析較爲常用的_Ref_count,即採用默認的刪除器和內存分配器的版本的類:

41.png

  • 這個類非常簡潔,由於採用默認的刪除和內存分配操作,因此只需要保留一個_Ty*類型的指針即可。
  • 第二個派生類_Ref_count_resource:

43.png

  • 這個類中我們見到了一個老朋友:_Compressed_pair。詳見unique_ptr那一節中的講解,這裏不再贅述。
  • 第三個派生類:

44.png

  • 可以看到,這裏有一個嵌套的_Compressed_pair。因爲刪除器和內存分配器都很有可能是空類。
  • 這些類具體的不同主要在兩個虛函數中如何釋放資源以及如何釋放自己,這裏不再贅述,應該很容易看懂。我們看一下這些派生類是什麼時候生成並被賦值給_Ptr_base基類的_Rep成員的。跳回shared_ptr的構造函數:

45.png

46.png

  • 以只接收一個指針_Ux*的構造函數爲例,該函數內部調用了_Setp函數。這裏的is_array<_Ty>{}生成了一個臨時對象,大家在源代碼裏跳過去能看到這裏是做了一個函數選擇,利用函數重載的功能,根據_Ty是否是一個數組類型將其分發給對應的重載函數。這種技巧在stl庫中應用的很多,具體名字被我給忘了。。。這裏不多解釋,接着看。

47.png

48.png

  • _Setp確實有兩個重載函數,如果_Ty是一個數組類型,繼續調用_Setpd(_Px,default_delete<_Ux[]>{})。這裏根據_Ty的類型我們選擇了正確的刪除器類型。

49.png

  • 在_Setpd函數中我們看到了_Ref_count_resource這個類的動態生成。
  • 如果_Ty不是一個數組類型呢?我們看一下_Setp的另一個版本,其中出現了_Ref_count這個類的動態生成。
  • 兩個版本均繼續調用了_Set_ptr_rep_and_enable_shared函數,區別就是生成的引用計數類不一樣。現在我們搞清楚了大致流程:根據構造函數的不同、模板參數的不同,shared_ptr生成對應的引用計數派生類傳入底層函數。_Set_ptr_rep_and_enable_shared這個函數後續做了哪些工作,因爲涉及到了weak_ptr和enable_shared_from_this這些類,這裏一時半會說不清楚。目前我們只要知道將_Px和_Dt賦值給了基類中那兩個成員變量指針即可。
  • 到目前爲止我們能夠明白:不同的shared_ptr對象之所以能夠共享資源,是因爲其每個對象都有一個指向該資源的指針_Ptr和一個指向引用計數類的指針_Rep,根據刪除和內存分配等不同要求引用計數類有多個派生類,在構造shared_ptr時會根據情況生成對應的派生類。
  • 我們以一個拷貝構造函數爲例,看shared_ptr是如何通過調用其基類提供的函數修改引用計數,達到共享資源的目的的:

50.png

51.png

  • 如果_Other._Rep不爲空指針,則調用其_Incref()函數,然後對_Ptr和_Rep進行賦值,_Incref()這個函數看名字其實能看出來,就是增加引用計數,前面提到過,這些函數能保證在不同線程中原子的修改引用計數,我們看下內部的實現過程:

52.png

53.png

  • 再往下就是和平臺實現及其密切的原生API了,我們的分析都到這一步爲止。
  • 我們再看一下shared_ptr的析構函數:

54.png

55.png

56.png

57.png

  • 析構函數中,如果_Uses減1之後等於0,則調用_Destroy()函數釋放資源,並調用_Decwref()函數對弱引用計數減1,由於弱引用的存在,哪怕資源已經釋放,也有可能有弱引用綁定到引用計數類上,所以這裏不能直接釋放引用計數類的資源,而是判斷弱引用計數是否爲0,如果_Weaks也爲0,那麼可以釋放引用計數類的資源,調用_Delete_this()。(注:這裏對弱引用計數進行自減一操作,後面會解釋爲什麼)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章