天天看點

C++(STL):03---智能指針之shared_ptr

一、shared_ptr類

  • 頭檔案:#include<memory>
  • 智能指針,是一個模闆。建立智能指針時,必須提供指針所指的類型
  • 如果當做前提條件判斷,則是檢測其是否為空
  1. shared_ptr<string> p1; //指向string
  2. shared_ptr<list<int>> p2;//指向int的list
  3. if(p1 && p1->empty())
  4. *p1="h1";

二、make_shared函數

  • 最安全的配置設定和使用動态記憶體的方法就是調用該函數
  • 此函數在記憶體中動态配置設定對象并初始化,傳回此對象的shared_ptr
//指向一個值為42的int的shared_ptr
shared_ptr<int> p = make_shared<int>(42);
//p2指向一個值為10個'9'的string
shared_ptr<string> p2=make_shared<string>(10, '9');
//p3指向一個值初始化為0的int數
shared_ptr<int> p3 = make_shared<int>();      
  • 配合auto使用:make_shared函數可以指派給auto,這樣比較簡單

auto p=make_shared<vector<string>>();

三、shared_ptr的拷貝、指派與引用計數

  • 引用計數:shared_ptr類所指向的對象都有一個引用計數
  • 但對shared_ptr類進行拷貝時,計數器就會增加。例如:當用一個shared_ptr初始化另一個shared_ptr、或者它作為參數傳遞給一個函數以及作為函數的傳回值,它所關聯的計數器就會增加
  • 當我們給讓shared_ptr指向另一個對象或者shared_ptr銷毀時,原對象的計數器就會遞減
  • 一旦一個shared_ptr的計數器為0,就會自動釋放該對象的記憶體
auto p=make_shared<int>(42); //p指向一個引用者
auto q(p); //用p初始化q,那麼p所指的對象計數器加1      
  1. auto r=make_shared<int>(42);
  2. r=q;
  • 将q指派給r,那麼:
  •  r原來所指的對象引用計數變為0,然後自動釋放記憶體
  • q所指的對象的引用計數+1

四、shared_ptr的自動銷毀對象記憶體機制

  • 由上面可知,當指向一個對象的最後一個shared_ptr對象被銷毀時,shared_ptr類會自動銷毀此對象。shared_ptr類是通過析構函數來完成銷毀工作的
  • 記憶體浪費:因為隻有在銷毀掉最後一個shared_ptr時,該指針所指向的記憶體才會釋放,是以如果你忘記了銷毀程式不再需要的shared_ptr,程式仍然正在執行,那麼就造成記憶體浪費

五、shared_ptr與作用域的關系

  • shared_ptr類所指向的記憶體何時被釋放,與shared_ptr類的生存周期有關

示範案例:

  • 首先我們定義下面的函數傳回一個指向于一個值的share_ptr指針
  1. shared_ptr<Foo> factory(T arg)
  2. {
  3. return make_share<Foo>(arg);//傳回一個share_ptr類型的智能指針
  4. }
  • 情景一:例如下面函數調用factory函數來生成一個shared_ptr指針,但是p一旦離開了作用域(use_factory函數),那麼p指針就失效了,是以p所指向的記憶體位址也就自動釋放了
  1. //函數結束之後,p就自動釋放它所指向的對象的記憶體
  2. void use_factory(T arg)
  3. {
  4. shared_ptr<Foo> p=factory(arg);
  5. }
  • 情景二:下面的函數也是 factory函數來生成一個shared_ptr指針,但是p指針通過傳回值傳回了,是以,如果有另一個shared_ptr指針調用了該函數,那麼該p所指向的記憶體位址不會随着use_factory函數的調用而釋放
  1. auto use_factory(T arg)
  2. {
  3. shared_ptr<Foo> p=factory(arg);
  4. return p;
  5. }

六、shared_ptr與new的使用

使用規則:

  • ①我們可以使用将shared_ptr類對象指向一個new所申請的動态記憶體
  • ②new申請的動态記憶體的使用、釋放等規則仍然符合shared_ptr類的使用規則

使用文法:

  • 因為智能指針的構造函數是explicit的。是以:我們不能将一個内置指針隐式地轉換為一個智能指針,必須使用直接初始化形式來初始化一個智能指針
  1. shared_ptr<int> p=new int(1024); //錯誤
  2. shared_ptr<int> p2(new int(1024)); //正确:使用直接初始化
  • 動态記憶體作為傳回值時的使用手法:限于上面的使用文法,一個傳回shared_ptr的函數不能在其傳回語句中隐式轉換為一個普通指針 
  1. shared_ptr<int> clone(int p)
  2. {
  3. return new int(p); //錯誤
  4. }
  5. shared_ptr<int> clone(int p)
  6. {
  7. return shared_ptr<int>(new int(p)); //正确
  8. }

七、shared_ptr類的函數傳參使用

  • 當一個函數的參數是shared_ptr類時,有以下規則:
  • 函數的調用是傳值調用
  • 調用函數時,該shared_ptr類所指向的對象引用計數加1。但是函數調用完成之後,shared_ptr類自動釋放,對象的引用計數又減1
  1. void process(shared_ptr<int> ptr){ ... }
  2. shared_ptr<int> p(new int(42)); //初始化一個智能指針對象p
  3. process(p); //p所指的對象引用計數加1
  4. //process函數調用之後,p所指的引用計數減1
  5. int i=*p; //正确

函數參數使用時與new的關系:

  • 因為shared_ptr類會在生存周期結束之後,将引用計數減1,當引用計數為0時,會釋放記憶體空間
  •  下面是一個特殊的應用場景,需要注意
  1. void process(shared_ptr<int> ptr){ ... }
  2. int *x(new int(1024));
  3. process(x); //錯誤,不能将int*轉換為一個shared_ptr<int>
  4. process(shared_ptr<int>(x)); //合法的,但是process函數傳回之後記憶體會被釋放
  5. int j=*x; //錯誤,x所指的記憶體已經被釋放了

八、get函數的使用

  • shared_prt類的get函數傳回一個内置指針,指向智能指針所管理的對象
  • 此函數的設計情況:我們需要向不能使用智能指針的代碼傳遞一個内置指針
  • get函數将記憶體的通路權限傳遞給一個指針,但是之後代碼不會delete該記憶體的情況下,對get函數的使用才是最安全的
  • 永遠不要用get初始化另一個智能指針或者為另一個智能指針指派
  1. shared_ptr<int> p(new int(42)); //引用計數變為1
  2. int *q=p.get(); //正确:使用q需要注意,不要讓它管理的指針被釋放
  3. {//新語句塊
  4. shared_ptr<int>(q); //用q初始化一個智能指針對象
  5. } //語句塊結束之後,智能指針對象釋放它所指的記憶體空間
  6. int foo=*p;//錯誤的,p所指的記憶體已經被釋放了

九、reset、unique函數的使用

  • reset函數會将shared_prt類原先所指的記憶體對象引用計數減1,并且指向于一塊新的記憶體
  1. shared_ptr<int> p;
  2. p=new int(1024); //錯誤:不能将一個指針賦予shared_ptr
  3. p=reset(new int(1034)); //正确,p指向一個新對象
  • reset函數與unqie函數配合使用:在改變對象之前,檢查自己是否為目前對象的唯一使用者
  1. shared_ptr<string> p=make_shared<string>("Hello");
  2. if(!p.unique()) //p所指向的對象還有别的智能指針所指
  3. p.reset(new string(*p)); //現在可以放心的改變p了
  4. *p+=newVal; //p所指向的對象隻有自己一個智能指針,現在可以放心的改變對象的值了

十、異常處理

  • 當程式發生異常時,我們可以捕獲異常來将資源被正确的釋放。但是如果沒有對異常進行處理,則有以下規則:
  • shared_ptr的異常處理:如果程式發生異常,并且過早的結束了,那麼智能指針也能確定在記憶體不再需要時将其釋放
  • new的異常處理:如果釋放記憶體在異常終止之後,那麼就造成記憶體浪費
  1. voif func()
  2. {
  3. shared_ptr<int> sp(new int(42));
  4. ...//此時抛出異常,未捕獲,函數終止
  5. }//shared_ptr仍然會自動釋放記憶體
  6. voif func()
  7. {
  8. int *ip=new int(42);
  9. ...//此時抛出異常,未捕獲
  10. delete ip; //在退出之前釋放記憶體,此語句沒有執行到,導緻記憶體浪費
  11. }

十一、重置shared_prt類删除器

  • 概念:前面介紹過,當shared_ptr生命周期結束時,會調用預設的析構函數來釋放(delete)自己所指向的記憶體空間。但是我們可以使用shared_prt的文法來指定删除器函數,那麼在shared_ptr生命周期結束時就會自動調用這個函數 

示範案例:

  • 下面示範一個shared_ptr指定删除器函數以及避免記憶體洩露的案例
  • 錯誤情景:我們調用f函數來打開一個網絡連接配接,但是在f函數調用之後沒有關閉這個連接配接。是以就會造成記憶體的洩露 
  1. struct destination; //連接配接的對象
  2. struct connection; //連接配接需要的資訊
  3. connection connect(destbination*); //打開連接配接
  4. void disconnect(connection); //關閉連接配接
  5. void f(destination &d)
  6. {
  7. connection c=connect(&d);//打開一個連接配接
  8. ....//使用這個連接配接
  9. //如果在f函數退出之前忘記調用disconnect函數,那麼該連接配接就沒有關閉
  10. }
  • 正确情景:現在我們定義一個新的函數“end_connection”,并且配合shared_ptr類的使用。shared_ptr指定了一個删除器函數“end_connection”。是以下面的代碼能夠保證在f函數的各種調用結束時,保證連接配接正确的關閉
  1. void end_connection(connection *p)
  2. {
  3. disconnection (*p);
  4. }
  5. void f(destination &d)
  6. {
  7. connection c=connect(&d);
  8. shared_ptr<connection> p(&c,end_connection);
  9. ....//使用這個連接配接
  10. //當f函數退出或者異常退出,p都會調用end_connection函數
  11. }

十二、shared_prt與動态數組的使用 

  • 與unique_ptr不同,shared_ptr不直接支援管理動态數組。如果希望使用shared_ptr管理動态數組,必須提供自己定義的删除器
  • 如果未提供删除器,shared_ptr預設使用delete删除動态數組,此時delete少一個“[]”,因為會産生錯誤
//本例中,傳遞給shared_ptr一個lambda作為删除器




shared_ptr<int> sp(new int[10], [](int *p) { delete[] p; } );
shared_ptr<int> sp2(new int[3]{1,2,3}, [](int *p) { delete[] p; });
sp2.reset();  //使用自己書寫的lambda釋放數組      
  • 動态數組的通路:shared_ptr不支援點和箭頭成員運算符通路數組,并且不提供下标運算符通路數組,隻能通過get()函數來擷取一個内置指針,然後再通路數組元素
shared_ptr<int> sp(new int[3]{1,2,3}, [](int *p) { delete[] p; });
for (size_t i = 0; i != 3; ++i)
*(sp.get() + i) = i;