c++智慧指針介紹
- 2019 年 10 月 3 日
- 筆記
C++11標準引入了boost庫中的智慧指針,給C++開發時的記憶體管理提供了極大的方便。接下來這篇文件介紹shared_ptr/weak_ptr內部實現原理及使用細節。
C++不像java有記憶體回收機制,每次程式設計師new出來的對象需要手動delete,流程複雜時可能會漏掉delete,導致記憶體泄漏。於是C++引入智慧指針,可用於動態資源管理,資源即對象的管理策略。
C++中的shared_ptr/weak_ptr和Android的sp/wp功能類似,都為解決多執行緒編程中heap記憶體管理問題而產生的。當程式規模較小時,我們可以手動管理new分配出來的裸指針,什麼時候delete釋放我們自己手動控制。
但是,當程式規模變大時,並且該heap記憶體會在各個執行緒/模組中進行傳遞和互相引用,當各個模組退出時,誰去釋放?由此引入了智慧指針的概念。
其原理可以概況為,內部通過引用計數方式,指示heap記憶體對象的生存周期,而智慧指針變數作為一個stack變數,利用棧區變數由作業系統維護(程式設計師無法控制)的特點進行管理。
實現細節:
這樣的東東(我們姑且稱其為一個“類”,就像int/char/string為程式語言的內建類,我們也可以定義自己的類來使用)需要有什麼特點?
1.這個類內部需要有個指針,就是保護那個經常犯錯的裸指針:heap記憶體對象指針。
2.這個類能夠代表所有類型的指針,因此必須是模板類。
3.根據需要自動釋放其指向的heap記憶體對象,也即當這個“智慧指針類對象”釋放時,其內部所包含的heap記憶體對象根據需要進行釋放,因此這個類對象只能是一個stack區的對象(如果是heap區的,我們還需要手動delete,而我們希望有個系統能幫我們去做的東西),另外一點,這個類內部還需要有個變數,用於指示內部的heap記憶體對象引用數量,以便決定是否釋放該heap記憶體對象。
智慧指針shared_ptr對象跟其它stack區對象一樣有共同的特點——每次離開作用域時會自動調用析構函數進行記憶體回收。利用該特點,析構時檢查其內部所引用的heap記憶體對象的引用數量進行操作:1.引用計數減一變為0時,則必須釋放;2.減一後仍不為0,那麼其內部的heap記憶體對象同時被別的智慧指針引用,因此不能釋放。
使用示例:
1 #include <iostream> 2 #include <memory> 3 #include <string> 4 5 using namespace std; 6 7 class Person { 8 public: 9 Person() { 10 cout<<"Person ctor"<<endl; 11 } 12 13 Person(const string &alias): name(alias) { 14 cout<<"Person ctor for "<<name.c_str()<<endl; 15 } 16 17 ~Person() { 18 cout<<"Person dtor for "<<name.c_str()<<endl; 19 } 20 21 void setFather(shared_ptr<Person> &p) { 22 father = p; 23 } 24 25 void setSon(shared_ptr<Person> &p) { 26 son = p; 27 } 28 29 void printName() { 30 cout<<"name: "<<name.c_str()<<endl; 31 } 32 33 private: 34 const string name; 35 shared_ptr<Person> father; 36 shared_ptr<Person> son; 37 }; 38 39 void test0() 40 { 41 cout<<"---------test0 normal release begin---------"<<endl; 42 43 shared_ptr<Person> sp_pf(new Person("zjz")); 44 shared_ptr<Person> sp_ps(new Person("zcx")); 45 46 cout<<"---------test0 normal release end---------n"<<endl; 47 } 48 49 void test1() 50 { 51 cout<<"n---------test1 no release begin---------"<<endl; 52 53 shared_ptr<Person> sp_pf(new Person("zjz")); 54 shared_ptr<Person> sp_ps(new Person("zcx")); 55 56 cout<<"addr: "<<&sp_pf<<endl; 57 cout<<"addr: "<<&sp_ps<<endl; 58 59 cout<<"111 father use_count: "<<sp_pf.use_count()<<endl; 60 cout<<"111 son use_count: "<<sp_ps.use_count()<<endl; 61 62 sp_pf->setSon(sp_ps); 63 sp_ps->setFather(sp_pf); 64 65 cout<<"222 father use_count: "<<sp_pf.use_count()<<endl; 66 cout<<"222 son use_count: "<<sp_ps.use_count()<<endl; 67 68 cout<<"---------test1 no release end---------n"<<endl; 69 } 70 71 void test2() 72 { 73 cout<<"---------test2 release sequence begin---------"<<endl; 74 75 shared_ptr<Person> sp_pf(new Person("zjz")); 76 shared_ptr<Person> sp_ps(new Person("zcx")); 77 78 cout<<"addr: "<<&sp_pf<<endl; 79 cout<<"addr: "<<&sp_ps<<endl; 80 81 cout<<"111 father use_count: "<<sp_pf.use_count()<<endl; 82 cout<<"111 son use_count: "<<sp_ps.use_count()<<endl; 83 84 sp_pf->setSon(sp_ps); 85 //sp_ps->setFather(sp_pf); 86 87 cout<<"222 father use_count: "<<sp_pf.use_count()<<endl; 88 cout<<"222 son use_count: "<<sp_ps.use_count()<<endl; 89 90 cout<<"---------test2 release sequence end---------"<<endl; 91 } 92 93 int main(void) 94 { 95 test0(); 96 test1(); 97 test2(); 98 99 return 0; 100 }
其執行結果如下:
幾點解釋說明:
1.test0和test1中為什麼有dtor的列印?
sp_pf和sp_ps作為stack對象,當其離開作用域時自動由系統釋放,因此在輸出“test0 end”後test0()退出時,才會真正釋放stack object。
當其釋放時,檢查內部引用計數為1,則可以釋放引用的真正的heap記憶體——new Person("zjz")和new Person("zcx"),調用其析構函數。
2.釋放順序是什麼?為什麼test0和test2中不一樣?
sp_pf和sp_ps作為stack對象,我們可以回想stack對象記憶體管理方式——“先進後出,後進先出”,且地址變化由高地址向低地址過渡,由test1和test2中對sp_pf和sp_ps對象地址的列印資訊可以驗證。
那麼當退出時,肯定先釋放sp_ps對象,再釋放sp_pf對象。test1可以確認——先釋放zcx,再釋放zjz。
然而test2中好像正好顛倒,怎麼回事呢???
答案是,仍然成立!退出test2()時,先釋放sp_ps,再釋放sp_pf。
在釋放sp_ps時,發現其引用的這個記憶體對象new Person("zcx"),還同時被別人(sp_pf)引用,只能將son的引用計數減一,而不能釋放,因此什麼列印都沒有。
在釋放sp_pf時,先進入構造函數~Person(),再釋放其member var。因此先有列印資訊:“dtor zjz”,再釋放內部的另外成員變數(對象)——son,因此後有資訊“dtor zcx”。
這裡面涉及到C++中另外一個知識點:構造/析構先後順序。——讀者可以回顧類對象構造時,是先進入構造函數,還是先構造內部的member。析構和構造正好顛倒。寫一個demo進行了摺疊,讀者自己去驗證。

1 #include <iostream> 2 #include <memory> 3 #include <string> 4 5 using namespace std; 6 7 class tmp { 8 public: 9 tmp() { 10 cout<<"tmp ctor"<<endl; 11 } 12 ~tmp() { 13 cout<<"tmp dtor"<<endl; 14 } 15 }; 16 17 class Person { 18 public: 19 Person() { 20 cout<<"Person ctor"<<endl; 21 } 22 23 Person(const string &alias): name(alias) { 24 cout<<"Person ctor for "<<name.c_str()<<endl; 25 } 26 27 ~Person() { 28 cout<<"Person dtor for "<<name.c_str()<<endl; 29 } 30 31 void setFather(shared_ptr<Person> &p) { 32 father = p; 33 } 34 35 void setSon(shared_ptr<Person> &p) { 36 son = p; 37 } 38 39 void printName() { 40 cout<<"name: "<<name.c_str()<<endl; 41 } 42 43 private: 44 const string name; 45 shared_ptr<Person> father; 46 shared_ptr<Person> son; 47 tmp mtmp; 48 }; 49 50 int main(void) 51 { 52 Person *pp = new Person("sequence"); 53 delete pp; 54 55 return 0; 56 }
View Code
3.test1中的好像沒有釋放?
是的。在setFather/Son前,refCnt=1,之後變成了2。當退出test1()時,兩塊heap記憶體new Person("zjz")和new Person("zcx")的ref減一變成1,但都因互相引用對方而無法釋放。這時需要引入另外一種智慧指針——weak_ptr。
weak_ptr的引入:
weak_ptr是為配合shared_ptr而引入的一種智慧指針來協助shared_ptr工作,它可以從一個 shared_ptr 或另一個 weak_ptr 對象構造,它的構造和析構不會引起引用記數的增加或減少。沒有重載*和->但可以使用lock獲得一個可用的shared_ptr對象。
為什麼要引入“弱引用”指針呢?
weak_ptr和shared_ptr是為解決heap對象的“所有權”而來。弱引用指針就是沒有“所有權”的指針。有時候我只是想找個指向這塊記憶體的指針,但我不想把這塊記憶體的生命周期與這個指針關聯。這種情況下,弱引用指針就代表“我指向這東西,但這東西什麼時候釋放不關我事兒……”
使用區別:
首先,不要把智慧指針和祼指針的區別看得那麼大,它們都是指針。因此,我們可以把智慧指針和祼指針都統稱為指針,它們共同的目標是通過地址去代表資源。既然指針能代表資源,那麼不可避免地會涉及資源的所有權問題。在選擇具體指針類型的時候,通過問以下幾個問題就能知道使用哪種指針了。
1.指針是否需要擁有資源的所有權?
如果指針變數需要綁定資源的所有權,那麼會選擇unique_ptr或shared_ptr。它們可以通過RAII完成對資源生命期的自動管理。如果不需要擁有資源的所有權,那麼會選擇weak_ptr和raw pointer,這兩種指針變數在離開作用域時不會對其所指向的資源產生任何影響。
2.如果指針擁有資源的所有權(owning pointer),那麼該指針是否需要獨佔所有權?
獨佔則使用unique_ptr(人無我有,人有我丟),否則使用shared_ptr(你有我有全都有)。這一點很好理解。
3.如果不擁有資源的所有權(non-owning pointer),那麼指針變數是否需要在適當的時候感知到資源的有效性?
如果需要則使用weak_ptr,它可以在適當的時候通過weak_ptr::lock()獲得所有權,當擁有所有權後便可以得知資源的有效性。