善用shared_ptr,遠離記憶體泄漏(文末福利)
- 2019 年 12 月 16 日
- 筆記
來源:公眾號【編程珠璣】
作者:守望先生
ID:shouwangxiansheng

《為何優先選用unique_ptr而不是裸指針?》中說到,如果有可能就使用unique_ptr,然後很多時候對象是需要共享的,因此shared_ptr也就會用得很多。shared_ptr允許多個指向同一個對象,當指向對象的最後一個shared_ptr銷毀時,該對象也就會自動銷毀。因此,善用shared_ptr,能夠遠離記憶體泄漏。
基本使用
它的很多操作與unique_ptr類似。下面是三種常見的定義方式:
shared_ptr<int> sp;//聲明一個指向int類型的智慧指針 sp.reset(new int(42)); auto sp1 = make_shared<string>("hello");//sp1是一個智慧指針 shared_ptr sp2(new int(42));
而make_shared方式是推薦的一種,它使用一次分配,比較安全。
哪些操作會改變計數
我們都知道,當引用計數為0時,shared_ptr所管理的對象自動銷毀(擁抱智慧指針,告別記憶體泄露),那麼哪些情況會影響引用計數呢?
賦值
例如:
auto sp = make_shared<int>(1024);//sp的引用計數為1
再比如:
auto sp1 = make_shared<string>("obj1"); auto sp2 = make_shared<string>("obj2"); auto sp1 = sp2;
該操作會減少sp1的引用計數,增加sp2的引用計數。有的人可能不理解,為什麼這樣還會減少sp1的引用計數?
試想一下,sp1指向對象obj1,sp2指向對象obj2,那麼賦值之後,sp1也會指向obj2,那就是說指向obj1的就少了,指向obj2的就會多,如果此時沒有其他shared_ptr指向obj1,那麼obj1將會銷毀。
拷貝
例如:
auto sp2 = make_shared<int>(1024); auto sp1(sp2);
該操作會使得sp1和sp2都指向同一個對象。
而關於拷貝比較容易忽略的就是作為參數傳入函數:
auto sp2 = make_shared<int>(1024); func(sp2);//func的執行會增加其引用計數
可以看一個具體的例子:
//來源:公眾號編程珠璣 #include<iostream> #include<memory> void func0(std::shared_ptr<int> sp) { std::cout<<"fun0:"<<sp.use_count()<<std::endl; } void func1(std::shared_ptr<int> &sp) { std::cout<<"fun1:"<<sp.use_count()<<std::endl; } int main() { auto sp = std::make_shared<int>(1024); func0(sp); func1(sp); return 0; }
其運行輸出結果為:
fun0:2 fun1:1
很顯然,fun0,拷貝了shard_ptr sp,而fun1,並沒有拷貝,因此前者會增加引用計數,計數變為2,而後者並不影響。關於參數傳值的問題,可以參考《傳值與傳指針》和《令人疑惑的引用和指針》。
reset
調用reset會減少計數:
sp.reset()
而如果sp是唯一指向該對象的,則該對象被銷毀。
應當注意使用的方式
雖然shared_ptr能很大程度避免記憶體泄漏,但是使用不當,仍然可能導致意外發生。
存放於容器中的shared_ptr
如果你的容器中存放的是shared_ptr,而你後面又不再需要它時,記得使用erase刪除那些不要的元素,否則由於引用計數一直存在,其對象將始終得不到銷毀,除非容器本身被銷毀。
不要使用多個裸指針初始化多個shared_ptr
注意,下面方式是不該使用的:
#include<iostream> #include<memory> int main() { auto *p = new std::string("hello"); std::shared_ptr<std::string> sp1(p); /*不要這樣做!!*/ std::shared_ptr<std::string> sp2(p); return 0; }
這樣會導致兩個shared_ptr管理同一個對象,當其中一個被銷毀時,其管理的對象會被銷毀,而另外一個銷毀時,對象會二次銷毀,然而實際上,對象已經不在了,最終造成嚴重後果。
而與這種情況類似的,就是使用get()獲取裸指針,然後去初始化另外一個shared_ptr,或者delete get返回的指針:
//來源:公眾號【編程珠璣】 #include<iostream> #include<memory> int main() { auto sp = std::make_shared<std::string>("wechat:shouwangxiansheng"); std::string *p = sp.get(); std::shared_ptr<std::string> sp2(p);/*不要這樣做!!*/ delete p;/*不要這樣做*/ return 0; }
如果對象不是new分配的,請傳遞刪除器
與unique_ptr類似,它可以指定刪除器,默認是使用delete。例如:
//來源:公眾號【編程珠璣】 #include<iostream> #include<unistd.h> #include<memory> void myClose(int *fd) { close(*fd); } int main() { int socketFd = 10;//just for example std::shared_ptr<int> up(&socketFd,myClose); return 0; }
與unique_ptr的區別
首先最明顯的區別自然是它們一個是專享對象,一個是共享對象。而正是由於共享,包括要維護引用計數等,它帶來的開銷相比於unique_ptr來說要大。
另外,shared_ptr無法直接處理數組,因為它使用delete來銷毀對象,而對於數組,需要用delete[]。因此,需要指定刪除器:
/來源:公眾號【編程珠璣】 #include<iostream> #include<memory> int main() { auto sp = std::make_shared<std::string>("wechat:shouwangxiansheng"); std::string *p = sp.get(); //std::shared_ptr<int> sp1(new int[10]);//不能這樣 std::shared_ptr<int> sp1(new int[10],[](int *p){delete[] p;}); return 0; }
示例中使用了lambda表達式。
不過一般來說,好好的容器不用,為什麼要用動態數組呢?
總結
以上就是shared_ptr基本內容,一般來說,規範使用shared_ptr能很大程度避免記憶體泄露。注意,shared_ptr提供,*,->操作,不直接提供指針運算和[]。