智能指针 的理解
智能指針主要解決的問題
1、內存泄露:內存手動釋放,使用智能指針可以自動釋放malloc free ;new delete 2、共享所有權指針的傳播和釋放,比如多線程使用同一個對象時析構問題C++里面的四種智能指針:auto_ptr,shared_ptr,unique_ptr,weak_ptr其中后三個是C++支持,并且第一個已經被棄用
- unique_ptr 獨占對象的所有權,由于沒有引用計數,因此性能比較好
- shared_ptr 共享對象的所有權,但性能比較差
- weak_ptr 配合shared_ptr ,解決循環引用問題。
shared_ptr內存模型
shared_ptr 內部包含兩個指針,一個指向對象,另一個指向控制塊(control block),控制塊中包含一個引用計數(reference count),一個弱計數(weak count)和其它一些數據。
智能指針使用場景案例
使用智能指針可以自動釋放占用的空間
shared_ptr<Buffer> buf = make_shared<Buffer>("auto free memory"); // buf 對象分配在堆上,但能自動釋放 // 對比 Buffer *buf = new Buffer("auto free memory");//buf對象分配在堆上,但需要手動delete釋放共享所有權指針的傳播和釋放
shared_ptr 共享的智能指針
std::shared_ptr 使用引用計數器,每一個shared_ptr的拷貝都指向相同的內存。再最后一個shared_ptr析構的時候,內存才會被釋放。
shared_ptr 共享被管理對象,同一時刻可以有多個shared_ptr 擁有對象的所有權,當最后一個shared_ptr對象銷毀時,被管理對象自動銷毀。
簡單說,shared_ptr 實現包含了兩部分,
- 一個指向堆上創建的對象的裸指針,raw_ptr
- 一個指向內部隱藏的、共享的管理對象。share_count_obj
shared_ptr 的基本用法和常用函數
- s.get():返回shared_ptr中保存的裸指針;
- s.reset():重置shared_ptr;
- reset() :不帶參數時,若智能指針s是唯一指向該對象的指針,則釋放,并置空。若智能指針P不是唯一指向該對象的指針,則引用計數減少1,同時將P置空。
- reset():帶參數時,若智能指針s是唯一指向對象的指針,則釋放并指向新的對象。若P不是唯一的指針,則只減少引用計數,并指向新的對象。
s.use_count():返回shared_ptr的強引用計數;
s.unique():若use_count為1,返回true,否則返回false。
初始化make_shared/reset
通過構造函數、std::shared_ptr輔助函數和reset方法來初始化shared_ptr,代碼如下:
std::shared_ptr<int> p1(new int(1)); std::shared_ptr<int> p2 = p1; std::shared_ptr<int> p3; if(p3) {cout << "p3 is not null"; }用make_shared來構建智能指針,因為make_shared比較高效。
auto sp1 = make_shared<int>(100); //或 shared_ptr<int> sp1 = make_shared<int>(100); //相當于 shared_ptr<int> sp1(new int(100));不能將一個原始指針直接賦值給一個智能指針,例如:
std::shared_ptr<int> p = new int(1);shared_ptr 不能通過“直接將原始這種賦值”來初始化,需要通過構造函數和輔助方法來初始化
- 對于一個未初始化的智能指針,可以通過reset方法來初始化;
- 當智能指針有值的時候調用reset會引起引用計數減1
另外智能指針可以通過重載的bool類型操作符來判斷。
int main {std::shared_ptr<int> p1;p1.reset(new int(1));std::shared_ptr<int> p2 = p1;// p2.use_count() = 2cout << "p2.use_count() = " << p2.use_count() << endl;p1.reset();cout << "p1.reset()\n";// p2.use_count() = 1cout << "p2.use_count()= " << p2.use_count() << endl;if (!p1) {cout << "p1 is empty\n"; // 執行了}if (!p2) {cout << "p2 is empty\n";}p2.reset();cout << "p2.reset()\n";// p2.use_count() = 0cout << "p2.use_count()= " << p2.use_count() << endl;if (!p2) {cout << "p2 is empty\n"; // 執行了} }獲取原始指針get
當需要獲取原始指針時,可以通過get方法來返回原始指針:
std::shared_ptr<int> ptr(new int(1)); int *p = ptr.get(); // 獲取盡量不要使用p.get()的返回值,如果不知道其危險性則永遠不要調用get() 函數。
指定刪除器
如果用shared_ptr 管理非new對象或是沒有析構函數的類時,應當為其傳遞合適的刪除器。
#include <iostream> #include <memory> using namespace std; void DeleteIntPtr(int *p) {cout <<"call DeleteIntPtr" << endl;delete p; } int main() {std::shared_ptr<int> p(new int(1),DeleteIntPtr);return 0; }當p的引用計數為0時,自動調用刪除器DeleteIntPtr來釋放對象的內存。刪除器可以是一個lambda表達式,如:
std::shared_ptr<int> p(new int(1),[](int *p) {cout << "call lambda delete p " << endl;delete p; })當用shared_ptr管理動態數組時,需要指定刪除器,因為shared_ptr的默認刪除器不支持數組對象,代碼如:
std::shared_ptr<int> p3(new int[10],[](int *p) {delete [] p;})使用shared_ptr 要注意的問題
不要用一個原始指針初始化多個shared_ptr
int *ptr = new int; shared_ptr<int> p1(ptr); shared_ptr<int> p2(ptr); // 邏輯錯誤不要在函數實參中創建shared_ptr
func(shared_ptr<int>(new int),g()); // 有缺陷因為c++的函數參數的計算順序在不同的編譯器不同的約定下可能不一樣的。 一般是從右到左,但也可能從左到右,所以,可能的過程是先new int,然后調用g(),如果恰好g()發生異常,而shared_ptr還沒有創建,則int內存就泄露了, 正確寫法如下:
shared_ptr<int> p(new int); func(p,g());通過shared_from_this() 返回this指針
不要將this指針作為shared_ptr 返回出來,因為this指針本質上是一個裸指針,因此,這樣可能會導致重復析構,如:
#include <iostream> #include <memory> using namespace std; class A { public: shared_ptr<A> GetSelf() { return shared_ptr<A>(this); // 不要這么做 } ~A() { cout << "Destructor A" << endl; } }; int main() { shared_ptr<A> sp1(new A); shared_ptr<A> sp2 = sp1->GetSelf(); return 0; }運行后調用了兩次析構函數。
在例子中,由于用同一個指針(this)構造了兩個智能指針sp1和sp2,而他們之間是沒有任何關系的,在離開作用域之后this將會被構造的兩個智能指針各自析構,導致重復析構的錯誤。
正確返回this和shared_ptr的做法是: 讓目標類通過std::enable_shared_from_this類,然后使用基類的成員函數 shared_from_this()來返回this的shared_ptr,如下:
在weak_ptr章節我們繼續講解使用shared_from_this() 的原因。
避免循環引用
循環引用會導致內存泄露,如:
#include <iostream> #include <memory>using namespace std;class A; class B; class A { public:std::shared_ptr<B> bptr;~A() {cout << "A is deleted" << endl;} }; class B { public:std::shared_ptr<A> aptr;~B() {cout << "B is deleted" << endl;} }; int main() {{std::shared_ptr<A> ap(new A);std::shared_ptr<A> bp(new B);ap->bptr = bp;bp->aptr = ap;}cout << "main leave" << endl; // 循環引用導致ap bp 退出了作用域都沒有析構return 0; }循環引用導致ap和bp的應用計數為2,在離開作用域之后,ap和bp的引用計數減為1,并不會減0,導致兩個指針都不會被析構,產生內存泄露。
解決的辦法是把A和B任何一個成員變量改為weak_ptr,具體方法見weak_ptr
unique_ptr 獨占的智能指針
unique_ptr是一個獨占型的智能指針,它不允許其他的智能指針共享其內部的指針,不允許通過賦值將一個unique_ptr賦值給另一個unique_ptr,如:
unique_ptr<T> my_ptr(new T); unique_ptr<T> my_other_ptr = my_ptr; // 報錯,不能復制unique_ptr不允許復制,但可以通過函數返回給其它unique_ptr,還可以通過std::move來轉移到其他的unique_ptr,這樣它本身就不再擁有原來指針的所有權了,如:
unique_ptr<T> my_ptr(new T) ; // 正確 unique_ptr<T> my_other_ptr = std::move(my_ptr); // 正確 unique_ptr<T> ptr = my_ptr; //報錯,不能復制std::make_shared 是c++的一部分,但std::make_unique不是。
auto upw1(std::make_unique<widget>()); std::unique_ptr<widget> upw2(new widget);除了unique_ptr 的獨占性,unique_ptr和shared_ptr 還有一些區別,比如:
- unique_ptr可以指向一個數組,代碼如下:
- unique_ptr 指定刪除器和shared_ptr 有區別
- unique_ptr需要確定刪除器的類型,所以不能像shared_ptr 那樣直接指定刪除器,可以這樣寫:
關于shared_ptr 和unique_ptr 的使用場景是要根據實際應用需求來選擇。
如果希望只有一個智能指針管理資源或者管理數組就用unique_ptr,如果希望多個智能指針管理同一個資源就用shared_ptr。
weak_ptr 弱引用的智能指針
weak_ptr 是一種 不控制對象生命周期的智能指針,它指向一個shared_ptr 管理的對象,進行該對象的內存管理的那個強引用的shared_ptr,weak_ptr只提供了對管理對象的一種訪問手段。
weak_ptr 設計的目的是為配合shared_ptr 而引入的一種智能指針來協助shared_ptr 工作,它只可以從一個shared_ptr或另一個weak_ptr對象構造,它的構造和析構不會引起引用計數的增加或減少。
weak_ptr 的基本用法
結果輸出:
gw有效, *spt = 42 gw無效,資源已釋放weak_ptr 返回this指針
shared_ptr章節中提到不能直接將this指針返回shared_ptr,需要通過派生
std::enable_shared_from_this類,并通過其方法shared_from_this來返回指針,原因是
std::enable_shared_from_this類中有一個weak_ptr,這個weak_ptr用來觀察this智能指針,調用
shared_from_this()方法是,會調用內部這個weak_ptr的lock()方法,將所觀察的shared_ptr返回,再看
前面的范例:
輸出結果如下:
Destructor A
在外面創建A對象的智能指針和通過對象返回this的智能指針都是安全的,因為shared_from_this()是內
部的weak_ptr調用lock()方法之后返回的智能指針,在離開作用域之后,spy的引用計數減為0,A對象會
被析構,不會出現A對象被析構兩次的問題。
需要注意的是,獲取自身智能指針的函數盡在shared_ptr的構造函數被調用之后才能使用,因為
enable_shared_from_this內部的weak_ptr只有通過shared_ptr才能構造。
weak_ptr 解決循環引用問題
在shared_ptr章節提到智能指針循環引用的問題,因為智能指針的循環引用會導致內存泄漏,可以通過
weak_ptr解決該問題,只要將A或B的任意一個成員變量改為weak_ptr
這樣在對B的成員賦值時,即執行bp->aptr=ap;時,由于aptr是weak_ptr,它并不會增加引用計數,所
以ap的引用計數仍然會是1,在離開作用域之后,ap的引用計數為減為0,A指針會被析構,析構后其內
部的bptr的引用計數會被減為1,然后在離開作用域后bp引用計數又從1減為0,B對象也被析構,不會發生內存泄漏。
weak_ptr 使用注意事件
weak_ptr 在使用前需要檢查合法性。
weak_ptr<int> wp; { shared_ptr<int> sp(new int(1)); //sp.use_count()==1 wp = sp; //wp不會改變引用計數,所以sp.use_count()==1 shared_ptr<int> sp_ok = wp.lock(); //wp沒有重載->操作符。只能這樣取所指向的對象 } shared_ptr<int> sp_null = wp.lock(); //sp_null .use_count()==0因為上述代碼中sp和sp_ok離開了作用域,其容納的K對象已經被釋放了。
得到了一個容納NULL指針的sp_null對象。在使用wp前需要調用wp.expired()函數判斷一下。
因為wp還仍舊存在,雖然引用計數等于0,仍有某處“全局”性的存儲塊保存著這個計數信息。直到最后
一個weak_ptr對象被析構,這塊“堆”存儲塊才能被回收。否則weak_ptr無法直到自己所容納的那個指針
資源的當前狀態。
如果shared_ptr sp_ok和weak_ptr wp;屬于同一個作用域呢?如下所示:
weak_ptr<int> wp;shared_ptr<int> sp_ok;{shared_ptr<int> sp(new int(1)); //sp.use_count()==1wp = sp; //wp不會改變引用計數,所以sp.use_count()==1sp_ok = wp.lock(); //wp沒有重載->操作符。只能這樣取所指向的對象}if(wp.expired()) {cout << "shared_ptr is destroy" << endl;} else {cout << "shared_ptr no destroy" << endl;}智能指針安全性問題
引用計數本身是安全的,至于智能指針是否安全需要結合實際使用分情況討論:
情況1:多線程代碼操作的是同一個shared_ptr的對象,此時是不安全的。
比如std::thread的回調函數,是一個lambda表達式,其中引用捕獲了一個shared_ptr
情況2:多線程代碼操作的不是同一個shared_ptr的對象
這里指的是管理的數據是同一份,而shared_ptr不是同一個對象。比如多線程回調的lambda的是按值捕
獲的對象。
這時候每個線程內看到的sp,他們所管理的是同一份數據,用的是同一個引用計數。但是各自是不同的
對象,當發生多線程中修改sp指向的操作的時候,是不會出現非預期的異常行為的。
也就是說,如下操作是安全的。
需要注意:所管理數據的線程安全性問題。顯而易見,所管理的對象必然不是線程安全的,必然 sp1、
sp2、sp3智能指針實際都是指向對象A, 三個線程同時操作對象A,那對象的數據安全必然是需要對象
A自己去保證。
總結
- 上一篇: 学习笔记-写论文修改语法、同义词替换、找
- 下一篇: 以现代化基础架构拥抱新零售时代