善用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提供,*,->操作,不直接提供指針運算和[]。