C++內存管理之shared_ptr


   

 ----------------------------------------shared_ptr---------------------------------------

 

引子

  c++中動態內存的管理是通過new和delete來完成的,只要保證new和delete的配對使用,是沒有問題的。但是有時候我們會忘記釋放內存,甚至有時候我們根本就不知道什么時候釋放內存。特別時在多個線程間共享數據時,更難判斷內存該何使釋放。這種情況下就機器容易產生引用非法內存的指針。                                                                                                    

  為了更容易(同時也更安全的管)的使用動態內存,新的標准庫(C++11)提供了兩種智能指針(smart pointer)類型來管理動態對象。智能指針的行為類似於常規指針。重要的區別是它負責自動釋放所指向的對象。新標准提供的這兩種智能指針的區別在於管理底層指針的方式:shared_ptr允許多個指針指向同一個對象;unique_ptr則獨占所指向的對象。標准庫還定義了一個weak_ptr的伴隨類,他是一種弱引用,指向shared_ptr所管理的對象。這三種類型都定義在memory頭文件中。

 

 

 

 

 

 

 

 

 初始化 sahred_ptr

  智能指針的使用方式與普通指針類似。解引用一個智能指針返回它指向的對象。如果在一個條件判斷中使用智能指針,效果就是檢測它是否為空:

 

#include <iostream> using namespace std; int main() {  
  /*---------空指針------------*/
   shared_ptr<string> p1;
 if(!p1)     //!默認初始化的智能指針中保存着一個空指針!並不是""空字符串 cout<<"p1==NULL"<<endl;

  /*---------初始化------------*/
  shared_ptr<string> p2(new string); 
  
if(p2&&p2->empty()){         //!需要注意的時empty時屬於string的成員函數。
    
*p2="helloworld";
    cout
<<*p2<<endl;
  }
// shared_ptr<int> pa = new int(1);//!error:不允許以暴露裸漏的指針進行賦值操作。

  //一般的初始化方式 shared_ptr<string> pint(new string("normal usage!")); cout<<*pint<<endl; //推薦的安全的初始化方式 shared_ptr<string> pint1 = make_shared<string>("safe uage!"); cout<<*pint1<<endl;
}

 

 關於其它初始化智能指針的方法,如下;不推薦!在這里展示出來是希望極力避免不安全的使用范例。

    /*不推薦*/
    int * p = new int(32);
    shared_ptr<int> pp(p);
    cout<<*pp<<endl;

    /*意外的情況*/
//    delete p;               //!不小心把delete掉了。
//    cout<<*pp<<endl;·       //!pp也不再有效。

 

 

關於get()函數;

智能指針定義了一個名為get的函數,它返回一個內置指針,指向智能指針的管理的對象。此函數設置的初衷是當我們向不能使用智能指針的代碼傳遞一個內置指針。使用get返回指針的代碼不能delete此指針。

#include <iostream> #include <memory>
using namespace std; void useShared_ptr(int *p) { cout<<*p<<endl; } void delePointer(int *p) { delete p; } int main(int argc, char *argv[]) { shared_ptr<int> p1 = make_shared<int>(32); // shared_ptr<int>p2(p1.get()); //!錯誤的用法:但是p1、p2各自保留了對一段內存的引用計數,其中有一個引用計數耗盡,資源也就釋放了。 useShared_ptr(p1.get()); // delePointer(p1.get()); //!error: return 0; }

 再次聲明:get用來將指針的訪問權限傳遞給代碼,只有在確定代碼不會delete指針的情況下,才能使用get。特別是,永遠不要用get初始化另一個智能指針或者為另一個智能指針賦值!

 

 

 關於mak_shared函數:

  最安全的分配和使用動態內存的方法是調用一個名為make_shared的標准庫函數,此函數在動態內存中分配一個對象並初始化它,返回此對象的shared_ptr。與只能指針一樣,make_shared也定義在頭文件memory中。

 

 
         
#include <iostream>
using namespace std; int main() { shared_ptr<int> p3 = make_shared<int>(42); cout<<*p3<<endl; shared_ptr<string> pstr = make_shared<string>("99999"); cout<<*pstr<<endl; shared_ptr<int> pint = make_shared<int>(); //!默認初始化為 0 cout<<*pint<<endl; auto pau = make_shared<string>("auto"); //!更簡單,更常用的方式。 cout<<*pau<<endl; }

  使用make_shared用其參數來構造給定類型的對象;傳遞的參數必須能夠與該類型的某個構造函數相匹配

  通常我們用auto來定義一個對象來保存make_shared的結果,這種方式更為簡單。

 

shared_ptr的拷貝和賦值

  當進行拷貝或者賦值操作時,每個shared_ptr都會記錄有多少個其他的shared_ptr指向相同的對象:

 
         
#include <iostream>
using namespace std; int main() { auto p = make_shared<int>(42); //!p指向的對象只有p一個引用者。 cout<<p.use_count()<<endl; auto q(p); //!p和q指向相同的對象,此對象有兩個引用者。 cout<<p.use_count()<<endl; return 0; }

 

 

shared_ptr作返回值:

 
         
#include <iostream>
using namespace std; shared_ptr<string> factory(const char* p){ return make_shared<string>(p); } void use_factory(){ shared_ptr<string> p = factory("helloworld"); cout<<*p<<endl;          //!離開作用域時,p引用的對象被銷毀。

}
shared_ptr<string> return_share_ptr()
{
  shared_ptr<string> p = factory("helloworld");
  cout<<*p<<endl;
  return p;               //!返回p時,引用計數進行了遞增操作。
}                      //!p離開了作用域,但他指向的內存不會被釋放掉。




int main()
{
  use_factory();
  auto p = return_share_ptr();
  cout<<p.use_count()<<endl;
}

 

 

引用計數:

  可以認為每個shared_ptr都有一個關聯的計數器,通常稱其為引用計數。無論何時我們拷貝一個shared_ptr,計數器都會遞增。例如,當用一個shared_ptr去初始化另一個shared_ptr;當我們給shared_ptr賦予一個新的值或者是shared_ptr被銷毀(例如一個局部的shared_ptr離開其作用域)時,計數器就會遞減。一旦一個shared_ptr的計數器變為0,他就會自動釋放自己所管理的對象。

#include <iostream>
using
namespace std; int main() { auto p = make_shared<int>(42); //!指向的對象只有p一個引用者。 cout<<p.use_count()<<endl; auto q = make_shared<int>(56);//!指向的對象只有q一個引用者。 cout<<q.use_count()<<endl; cout<<"---------afterAssin-----"<<endl; p = q; //!p原來引用的對象經過賦值之后釋放掉了,q引用的對象有了p和q兩個引用。 cout<<*p<<"=="<<*q<<endl; cout<<q.use_count()<<endl; }

 

其他shared_ptr操作

  shared_ptr還定義了一些其他的操作,參考前面的shared_ptr操作表格,例如,我們可以用reset將一個 新的指針賦予一個shared_ptr:

 

#include <iostream> #include <memory> using namespace std; int main() { shared_ptr<string> p1(new string("helloworld--1")); // p1 = new string("helloworld2--2");//error! p1.reset(new string("helloworld2--2")); cout<<*p1<<endl; }

 

  與賦值類似,reset會更新(-1)引用計數,如果需要的話,會釋放p1指向的對象。reset成員經常與unique一起使用,來控制多個shared_ptr的共享對象。在改變底層對象之前,我們在檢查自己是否是當前對象僅有的用戶。如果不是,在改變之前要做一份新的拷貝:

#include <iostream> #include <memory> using namespace std; int main() { shared_ptr<string> p1(new string("helloworld--1")); shared_ptr<string> p2(p1); if(p1.unique()) cout<<*p1 + string(" is unique!")<<endl; else{ p1.reset(new string("new reset!")); cout<<*p1<<endl; } }

 

 

 

 

容器中的shared_ptr-記得用erease節省內存 

  對於一塊內存,shared_ptr類保證只要有任何shared_ptr對象引用它,他就不會被釋放掉。由於這個特性,保證shared_ptr在不用之后不再保留就非常重要了,通常這個過程能夠自動執行而不需要人工干預,有一種例外就是我們將shared_ptr放在了容器中。所以永遠不要忘記erease不用的shared_ptr。

 
         
#include <iostream>
using namespace std;
int main() { list<shared_ptr<string>>pstrList; pstrList.push_back(make_shared<string>("1111")); pstrList.push_back(make_shared<string>("2222")); pstrList.push_back(make_shared<string>("3333")); pstrList.push_back(make_shared<string>("4444")); for(auto p:pstrList) { if(*p == "3333"); { /*do some thing!*/ } cout<<*p<<endl; } /*包含"3333"的數據我們已經使用完了!*/ list<shared_ptr<string>>::iterator itr = pstrList.begin(); for(;itr!=pstrList.end();++itr) { if(**itr == "3333"){ cout<<**itr<<endl; pstrList.erase(itr); } } cout<<"-------------after remove------------"<<endl; for(auto p:pstrList) { cout<<*p<<endl; }

  while(1)
  {
    /*do somthing other works!*/
    /*遍歷
pstrList*/ //!這樣不僅節約了大量內存,也為容器的使用增加了效率
  
  }
 }

 

狀態共享——why use shared_ptr?

  使用shared_ptr在一個常見的原因是允許多個多個對象共享相同的狀態,而非多個對象獨立的拷貝!

#include <iostream> using namespace std; void copyCase() { list<string> v1({"1","b","d"}); list<string> v2 = v1; //!v1==v2占用兩段內存  v1.push_back("cc"); //!v1!=v2 for(auto &p:v1){ cout<<p<<endl; } cout<<"--------void copyCase()---------"<<endl; for(auto &p:v2){ cout<<p<<endl; } } //v1和v2分屬兩個不同的對象,一個改變不會影響的狀態。 void shareCase() { shared_ptr<list<string>> v1 = make_shared<list<string>>(2,"bb"); shared_ptr<list<string>> v2 = v1; (*v1).push_back("c2c"); for(auto &p:*v1){ cout<<p<<endl; } cout<<"----------shareCase()--------"<<endl; for(auto &p:*v2){ cout<<p<<endl; } } //v1和v2屬於一個對象的兩個引用,有引用計數為證,其內容的改變是統一的。 int main() { copyCase(); cout<<"++++++++++++++++"<<endl; shareCase(); }

 

 

智能指針與異常

 

  異常發生后,常規的動態內存常常不能正確釋放。但是如果使用智能指針,即程序過早結束,智能指針也能確保在內存不需要時將其釋放:

 

void f() { shared_ptr<int>sp(new int(42)) ; }

 

  函數的推出,要么有兩種情況,正常處理結束或者發生了異常,無論哪種情況,局部對象都會被銷毀。在上面的程序中,sp是一個shared_ptr,因此sp銷毀時會檢查引用計數。在此例中,sp是指向這塊內存的唯一指針。所以會被釋放掉。

 

  與之相對的,當發生異常時,我們直接管理的內存時不會自動釋放的,如果使用內置指針管理內存,且在new之后對應的delet之前發生異常,則內存不會釋放。

 

void f() { int *p = new int(42); //code//!異常拋出,且沒有在f()中被捕獲。 delete p; }

 

   如果在new和delete之間發生異常,且異常未在f()中捕獲,則內存就永遠不會被釋放了。

 

 

 

 

shared_ptr對象的銷毀

1)管理動態數組

  默認情況下,shared_ptr指向的動態的內存是使用delete來刪除的。這和我們手動去調用delete然后調用對象內部的析構函數是一樣的。與unique_ptr不同,shared_ptr不直接管理動態數組。如果希望使用shared_ptr管理一個動態數組,必須提供自定義的刪除器來替代delete 。

 

#include <iostream>
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(); //!構造次數==析構次數 無內存泄漏 }

  通過自定義刪除器的方式shared_ptr雖然管理的是一個動態數組。但是shard_ptr並不支持下標運算符的操作。而且智能指針類型不支持指針算術運算(不能取地址)。因此為了訪問數組中的元素,必須用get獲取一個內置指針,然后用它來訪問數組元素。

 

2)管理非常規動態對象

  某些情況下,有些動態內存也不是我們new出來的,如果要用shared_ptr管理這種動態內存,也要自定義刪除器。

#include <iostream>
#include <stdio.h> #include <memory> using namespace std; void closePf(FILE * pf) { cout<<"----close pf after works!----"<<endl; fclose(pf); } int main() { // FILE * fp2 = fopen("bin2.txt", "w"); // if(!pf) // return -1; // char *buf = "abcdefg"; // fwrite(buf, 8, 1, fp2); // fclose(fp2); 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 int file!-----"<<endl; }                 //!即可以避免異常發生后無法釋放內存的問題,也避免了很多人忘記執行fclose的問題。

  在這里可以設想一下TCP/IP中鏈接的打開和關閉的情況,同理都可以使用智能指針來管理。

 

 總結:

  最后總結一下上面所陳述的內容,也是shared_ptr使用的基本規范

1)不使用相同的內置指針值初始化(或reset)多個智能指針。

2)不delete get函數返回的指針。

3)如果你使用了get返回的指針,記住當最后一個對應的智能指針銷毀后,你的指針就變為無效了。

4)如果你使用智能指針管理的資源不是new分配的內存,記得傳遞給他一個刪除器。

 

 weak_ptr

 weakptr使用的比較少,如有興趣了解,請去參考該篇文章:https://www.cnblogs.com/DswCnblog/p/5628314.html

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM