C++11標準引入了boost庫中的智能指針,給C++開發時的內存管理提供了極大的方便。接下來這篇文件介紹shared_ptr/weak_ptr內部實現原理及使用細節。java
C++不像java有內存回收機制,每次程序員new出來的對象須要手動delete,流程複雜時可能會漏掉delete,致使內存泄漏。因而C++引入智能指針,可用於動態資源管理,資源即對象的管理策略。ios
C++中的shared_ptr/weak_ptr和Android的sp/wp功能相似,都爲解決多線程編程中heap內存管理問題而產生的。當程序規模較小時,咱們能夠手動管理new分配出來的裸指針,何時delete釋放咱們本身手動控制。程序員
可是,當程序規模變大時,而且該heap內存會在各個線程/模塊中進行傳遞和互相引用,當各個模塊退出時,誰去釋放?由此引入了智能指針的概念。編程
其原理能夠概況爲,內部經過引用計數方式,指示heap內存對象的生存週期,而智能指針變量做爲一個stack變量,利用棧區變量由操做系統維護(程序員沒法控制)的特色進行管理。多線程
實現細節:ide
這樣的東東(咱們姑且稱其爲一個「類」,就像int/char/string爲程序語言的內建類,咱們也能夠定義本身的類來使用)須要有什麼特色?函數
1.這個類內部須要有個指針,就是保護那個常常犯錯的裸指針:heap內存對象指針。spa
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 }
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()得到全部權,當擁有全部權後即可以得知資源的有效性。