引文:
C++對指針的管理提供了兩種解決問題的思路:
1.不允許多個對象管理一個指針
2.允許多個對象管理一個指針,但僅當管理這個指針的最后一個對象析構時才調用delete
ps:這兩種思路的共同點就是只允許delete一次,下面將討論的shared_ptr就是采用思路1實現的
ps:智能指針不是指針,而是類,可以實例化為一個對象,來管理裸指針
1.shared_ptr的實現原理:
shared_ptr最本質的功能:“當多個shared_ptr管理同一個指針,僅當最后一個shared_ptr析構時,指針才被delete”,該功能是通過引用計數法實現的
引用計數法的規則:
1)所有管理同一個裸指針的shared_ptr,都共享一個引用計數器
2)每當一個shared_ptr被賦值給其他shared_ptr時,這個共享的引用計數器就加1
3)每當一個shared_ptr析構或被用於管理其他裸指針時,這個引用計數器就減1
4)如果此時發現引用計數器為0,那么說明它是管理這個指針的最后一個shared_ptr了,於是我們釋放指針指向的資源
引用計數法的內部實現:
1)這個引用計數器保存在某個內部類型中,而這個內部類型對象在shared_ptr第一次構造時以指針的形式保存在shared_ptr中
2)shared_ptr重載了賦值運算符,在賦值和拷貝另一個shared_ptr時,這個指針被另一個shared_ptr共享
3)在引用計數歸0時,這個內部類型指針與shared_ptr管理的資源一起釋放
4)此外,為了保證線程安全,引用計數器的加1和減1都是原子操作,它保證了shared_ptr由多個線程共享時不會爆掉
2.shared_ptr的使用
#include<iostream> #include<stdio.h> #include<string> #include<memory> using namespace std; int main() { //初始化 方法1: shared_ptr<string> sptr1(new string("name")); //初始化 方法2: shared_ptr<string> sptr2=make_shared<string>("sex"); //初始化 方法3: int *p =new int(10); shared_ptr<int> sptr3(p); //這種初始化的方式很危險,delete p之后,strp3也不再有效 }
相關成員函數:
1)use_count:返回引用計數的個數
2)unique:返回是否獨占所有權(use_count=1)
3)swap:交換兩個share_ptr對象(即交換所擁有的對象)
4)reset:放棄內部對象的所有權或擁有對象的變更,會引起原有對象引用計數的減少
5)get:返回內部對象指針
3.引用計數最大的缺點:循環引用
下面是事故現場:
class Observer; // 前向聲明 class Subject { private: std::vector<shared_ptr<Observer>> observers; public: Subject() {} addObserver(shared_ptr<Observer> ob) { observers.push_back(ob); } // 其它代碼 }; class Observer { private: shared_ptr<Subject> object; public: Observer(shared_ptr<Object> obj) : object(obj) {} // 其它代碼 };
目標類subject連接這多個觀察者類,當某個事件發生時,目標類可以遍歷觀察者數組observers,對觀察者進行通知,而觀察者類中也保留着目標類的shared_ptr,這樣多個觀察者之間可以以目標類為橋梁進行溝通,除了會發生內存泄漏外,這還是一種很不錯的設計模式嘛……
這里產生內存泄漏的原因就是循環引用,循環引用指的是一個引用通過一系列的引用鏈,竟然引回到自身,在上面的例子中,subject->observer->subject就是這么一條環形引用鏈,假設我們程序中只有一個變量shared_ptr<sbuject> p,此時p指向的對象不僅通過shared_ptr引向自己,還通過它包含的observer中的object成員變量引回自己,於是它的引用計數是2,每個observer的引用計數都是1,當p析構時,它的引用計數2-1=1,大於0,其析構函數不會被調用,於是p和它包含的每個observer對象在程序結束時依然駐留在內存中,沒有被delete,從而造成了內存泄漏
4.采用weak_ptr(弱引用)解決循環引用的問題:
標准庫提供了std::weak_ptr,weak_ptr是shared_ptr的觀察者,它與一個shared_ptr綁定,但是卻不參與引用計數的計算,在需要時,它還能生成一個與它所觀察的shared_ptr共享引用計數器的新的shared_ptr,總而言之,weak_ptr的作用就是:在需要時生成一個與綁定的shared_ptr共享引用計數器的新shared_ptr,在其他時候不干擾綁定的shared_ptr的引用計數
weak_ptr相關成員函數:
1)lock:獲得一個和綁定的shared_ptr共享引用計數器的新的shared_ptr
2)expired:功能等價於判斷use_count是否等於0,但是速度更快
繼續引用上面subject和observer的例子,來解決循環引用的問題:
將上述例子中,observer中object成員的類型換成weak_ptr<subject>即可解決內存泄漏的問題,因為之前的observer中object成員的subject參與了引用計數,替換成weak_ptr<subject>之后沒有參與引用計數,這樣以來,p指向對象的引用計數為1,所以在p析構時,subject指針將被delete,其中包含的observer數組在析構時,內部的observer對象的引用計數也為0,所以他們也被deleete了,不存在內存泄漏的問題了
class Observer; // 前向聲明 class Subject { private: std::vector<shared_ptr<Observer>> observers; public: Subject() {} addObserver(shared_ptr<Observer> ob) { observers.push_back(ob); } // 其它代碼 }; class Observer { private: shared_ptr< weak_ptr<Subject> > object; public: Observer(shared_ptr<Object> obj) : object(obj) {} // 其它代碼 };
5.錯誤用法1:多個無關的shared_ptr管理同一個裸指針,有可能導致二次析構
int main() { int *a = new int(10); shared_ptr<int> p1(a); shared_ptr<int> p2(a); }
p1和p2管理同一個裸指針a,此時的p1和p2有着完全獨立的兩個引用計數器,所以p1析構的時候會將a析構一次,p2析構的時候也會將a析構一次,C++中不允許同一個東西被析構兩次,這樣會導致程序爆炸
為了避免這種情況,我們永遠不要將new用在shared_ptr構造函數列表以外的地方,或者干脆不用new,改用make_shared
另外,即使這樣,也有可能導致二次析構,比如我們采用shared_ptr的get函數獲得原始裸指針來構造另一個shared_ptr
class A { public: std::shared_ptr<A> getShared() { return std::shared_ptr<A>(this); } }; int main() { std::shared_ptr<A> pa = std::make_shared<A>(); std::shared_ptr<A> pbad = pa->getShared(); }
上面的樣例中,pa和pbad各自擁有一個獨立的引用計數器,也有可能會導致二次析構
總而言之:管理同一個資源的sahred_ptr,只能由同一個初始shared_ptr通過一系列賦值和拷貝構造得到,要確保其共享的是同一個引用計數器
6.錯誤用法2:直接用new構造多個shared_ptr作為實參,可能會導致內存泄漏
// 聲明 void f(A *p1, B *p2); // 使用 f(new A, new B);
上面的代碼很容易發生內存泄漏,假如new A先發生於new B,那么如果new B拋出異常,那么new A的分配將會發生泄漏
如果按照這種方式new多個share_ptr作為實參,依然會發生內存泄漏
//聲明 void f(shared_ptr<A> p1,shared_ptr<B> p2); //使用 f(shared_ptr<A> (new A),shared_ptr<B>(new B));
因為shared_ptr的構造有可能發生在new A和new B之后,這里涉及到C++操作的sequence after性質,該性質保證:
1)new A發生在shared_ptr<A>構造發生之前
2)new B發生在shared_ptr<B>構造發生之前
3)兩個shared_ptr的構造發生在函數f的調用之前
在滿足上面三條性質的前提下,各操作的順序可以任意執行
若不使用new而是使用make_shared來構造shared_ptr,那么就不會產生內存泄漏
//聲明 void f(shared_ptr<A> p1,shared_ptr<B> p2); //使用 f(make_shared<A>(),make_shared<B>());
原因很簡單,依然是sequence after性質,如果兩個函數的執行順序不確定,那么當一個函數執行時,另外一個函數不會執行,於是make_shared<A>的構造完成了,即使make_shared<B>的構造拋出了異常,那么A的資源也能夠被正確的釋放,和上面的情形相比較,make_shared保證了第二個new發生的時候,第一個new所分配的資源已經被shared_ptr管理起來了,所以在異常發生時,能夠正確的釋放資源
總結:請總是使用make_shared來生成shared_ptr
7.如果希望使用shared_ptr來管理動態數組,那么需要提供一個自定義的刪除器來代替delete
#include <iostream> #include<memory> using namespace std; class DelTest { public: DelTest(){ j= 0; cout<<" DelTest()"<<":"<<i++<<endl; } ~DelTest(){ i = 0; cout<<"~ DelTest()"<<":"<<i++<<endl; } static int i,j; }; int DelTest::i = 0; int DelTest::j = 0; void noDefine() { cout<<"no_define start running!"<<endl; shared_ptr<DelTest> p(new DelTest[10]); } void slefDefine() { cout<<"slefDefine start running!"<<endl; shared_ptr<DelTest> p(new DelTest[10],[](DelTest *p){delete[] p;});//!傳入lambada表達式代替delete操作。 } int main() { noDefine();//!構造10次,析構1次。內存泄漏。 cout<<"--------------------"<<endl; slefDefine();//!構造次數==析構次數 無內存泄漏 } /* 運行結果: no_define start running! DelTest():0 DelTest():1 DelTest():2 DelTest():3 DelTest():4 DelTest():5 DelTest():6 DelTest():7 DelTest():8 DelTest():9 ~ DelTest():0 -------------------- slefDefine start running! DelTest():1 DelTest():2 DelTest():3 DelTest():4 DelTest():5 DelTest():6 DelTest():7 DelTest():8 DelTest():9 DelTest():10 ~ DelTest():0 ~ DelTest():0 ~ DelTest():0 ~ DelTest():0 ~ DelTest():0 ~ DelTest():0 ~ DelTest():0 ~ DelTest():0 ~ DelTest():0 ~ DelTest():0 */
需要注意的是:雖然通過自定義刪除器的方式shared_ptr可以管理動態數組,但是shared_ptr並不支持下標運算符的操作,而且只能指針類型不支持指針算術運算(不能取地址),因此為了訪問數組中的元素,必須用get獲得一個原始內置裸指針,然后用它來訪問數組元素
樣例如下:
#include <iostream> #include<memory> using namespace std; class DelTest { public: DelTest(){ j= 0; x=i; cout<<" DelTest()"<<":"<<i++<<endl; } ~DelTest(){ i = 0; cout<<"~ DelTest()"<<":"<<i++<<endl; } static int i,j; int x; }; int DelTest::i = 0; int DelTest::j = 0; void noDefine() { cout<<"no_define start running!"<<endl; shared_ptr<DelTest> p(new DelTest[10]); } void slefDefine() { cout<<"slefDefine start running!"<<endl; shared_ptr<DelTest> p(new DelTest[10],[](DelTest *p){delete[] p;});//!傳入lambada表達式代替delete操作。 cout<<p.get()[4].x<<endl; } int main() { noDefine();//!構造10次,析構1次。內存泄漏。 cout<<"--------------------"<<endl; slefDefine();//!構造次數==析構次數 無內存泄漏 } /* 運行結果: no_define start running! DelTest():0 DelTest():1 DelTest():2 DelTest():3 DelTest():4 DelTest():5 DelTest():6 DelTest():7 DelTest():8 DelTest():9 ~ DelTest():0 -------------------- slefDefine start running! DelTest():1 DelTest():2 DelTest():3 DelTest():4 DelTest():5 DelTest():6 DelTest():7 DelTest():8 DelTest():9 DelTest():10 5 ~ DelTest():0 ~ DelTest():0 ~ DelTest():0 ~ DelTest():0 ~ DelTest():0 ~ DelTest():0 ~ DelTest():0 ~ DelTest():0 ~ DelTest():0 ~ DelTest():0 */
8.使用shared_ptr管理非常規的動態對象的時候,記得自定義刪除器
某些情況下,有些動態內存也不是我們new出來的,如果要使用shared_ptr管理這種動態內存,也要自定義刪除器
#include <iostream> #include <stdio.h> #include <memory> using namespace std; void closePf(FILE * pf)//即可以避免異常發生后無法釋放內存的問題,也避免了很多人忘記執行fclose { cout<<"----close pf after works!----"<<endl; fclose(pf); } int main() { shared_ptr<FILE> pf(fopen("bin2.txt", "w"),closePf); cout<<"*****start working****"<<endl; if(!pf) return -1; char *buf = "abcdefg"; fwrite(buf,8,1,pf.get());//確保fwrite不會刪除指針的情況下,可以將shared_ptr內置指針取出 cout<<"------write in file!-----"<<endl; } /* *****start working**** ------write in file!----- ----close pf after works!---- */
類比TCP/IP中連接打開和關閉的情況,同理都可以使用shared_ptr來管理
總結:
1)不用使用相同的內置/原始/裸指針初始化多個智能指針
2)不要delete get函數返回的指針
3)如果你使用了get返回的指針,記住當最后一個對應的智能指針銷毀后,你的指針就變為無效了
4)如果你使用的智能指針管理的資源不是new分配的內存,記得傳遞一個刪除器
5)請勿使用new構造多個shared_ptr作為實參,應該使用make_shared
6)存在循環引用關系時,請使用weak_ptr來保證不會產生內存泄漏