天天看點

[c++] c++中的智能指針

要點彙總:1)   #9 - 基類的析構函數必須是virtual,否則可能導緻析構調用鍊斷層。
智能指針的使用:1)使用 普通指針/其他已存在的智能指針/其他已存在的普通指針,對目前建立的智能指針進行初始化。            (建立 指針 指向目前已有記憶體)
                 2)使用 make_shared 建立全新的記憶體區,然後建立一個全新的智能指針指向它。                                (建立 記憶體空間 和 指針)
                 3)使用 智能指針的 reset 方法來進行 智能指針的 重定向 和 釋放                                           (重定向 和 銷毀) #1  ===========================================================================================================================
 智能指針智能指針有兩種: shared_ptr  和  unique_ptr
這兩種指針都是模闆類,是以原型為   xxx_ptr<T>
(!)注:智能指針是用來管理堆記憶體的,不是作為指針使用的
(!)智能指針是使用delete來釋放記憶體,是以釋放記憶體時的特性和delete一樣
#2  ===========================================================================================================================
(!)首先明确一點,智能指針“僅僅”用來“動态配置設定記憶體”,而不是用來當做指針使用!!!!
是以:
 int i = 100;    //或者其他類,比如 classA a;
 shared_ptr<int> sp  = make_shared<int>(i);上述語句實際上是使用i的值作為新配置設定記憶體的初始值。sp并沒有指向i,而是指向了新配置設定的堆,這個堆存放int型的數值100
上述語句不能算錯誤,但是需要了解  make_shared 動作實際上是取堆裡申請了記憶體的。
    (可以這樣了解,make_shared相當于 new ,尖括号指定需要配置設定記憶體的類型名,小括号指定作為拷貝構造傳遞給
       類型名的值,如果不指定,那麼使用類型的預設構造) 同理:
 classA *pa = new classA();
 shared_ptr<classA> sp = make_shred<classA>(*pa);    //使用classA的拷貝構造在堆裡建立一個新的對象,而不是使用pa指向的對象 ====== EXAMPLE 1 =======
#include "stdafx.h"
 #include <iostream>
 #include <memory>
 using namespace std;class A{
 public:
     A() = default;
     ~A(){ ; }
 public:
     int myint=10;
 };int _tmain(int argc, _TCHAR* argv[])
 {
     A* pa = new A();
     shared_ptr<A> sp = make_shared<A>(*pa);
     sp->myint = 100;    cout << pa->myint << endl;        //pa = 10 ,值并沒有改變,因為它是用 new 配置設定的堆
     cout << sp->myint << endl;        //sp = 100,值和pa不一樣,因為它是用 make_shared 配置設定的堆    getchar();
    return 0;
 }    注:小括号裡的内容必須與尖括号裡類型的某個構造函數相比對
 ====== EXAMPLE 2 ======
#include "stdafx.h"
 #include <iostream>
 #include <memory>
 using namespace std;class A{
 public:
     A() = default;
     ~A(){ ; }
 public:
     int myint=10;
 };int _tmain(int argc, _TCHAR* argv[])
 {
     shared_ptr<A> sp = make_shared<A>();        //使用 A 的預設構造函數    cout << sp.use_count() << endl;                //為1
    shared_ptr<A> sp1 = sp;                        //增加了sp指向記憶體區的計數
    cout << sp.use_count() << endl;                //為2 
     cout << sp1.use_count() << endl;            //為2    sp1 = NULL;
    cout << sp.use_count() << endl;                //為1
    getchar();
    //小結:可見智能共享指針的計數器是由系統自動管理的,具體内部實作可能是 靜态資料類型,這樣各個
     //        執行個體操作的就是同一個資料,當然,可能會涉及到線程安全的問題,這時候就要加鎖,這也伴随着
     //        效率略低和死鎖的風向    return 0;
 }#3  ===========================================================================================================================
(!)在使用共享指針時,我們隻需要操作指針對象,不要試圖去直接操作指針指向的内容。因為當所有指針都銷毀的時候,動态配置設定的記憶體會被
 自動釋放。 計數增加:
 1)初始化一個shared_ptr
 2)作為參數傳遞給一個函數
 3)作為函數傳回值計數減少:
 1)給shared_ptr新指向對象,替換舊對象,那麼就對象計數減少
 2)shared_ptr調用析構函數,比如局部變量離開作用域等等     小結:shared_ptr将對對象的記憶體管理轉變為對對指向對象指針的管理。而這個指針的獲得僅能通過shared_make來實作。是以,從
           某種意義上來說,更集中。
           #4  ===========================================================================================================================
共享指針的應用場景:
 1)程式不知道自己需要多少個對象???
 2)程式不知道所需要對象的準确類型???
 3)程式需要在多個對象之間共享資料,這有點像類的static成員變量,是所有類共享的。但是既然有static,為什麼還要共享指針??? (!!!)使用動态記憶體的一個常見原因是:允許多個對象共享相同的狀态。
(!!!)為什麼不做成static的?
 主要因為static資料存放在.bss區,無法手動釋放,在程式運作時就載入,而這部分的可用空間是有限而狹小的。如果我們想要使用堆棧這樣
 寬敞的空間來模拟static成員,此時就可以使用共享指針管理的動态記憶體(堆)來實作,實作一個僅在大家都不在需要時才會釋放的堆區域。 #5  ===========================================================================================================================
引用計數法的内部實作:
  1)這個引用計數器儲存在某個内部類型中,而這個内部類型對象在shared_ptr第一次構造時以指針的形式儲存在shared_ptr中
  2)shared_ptr重載了指派運算符,在指派和拷貝另一個shared_ptr時,這個指針被另一個shared_ptr共享
  3)在引用計數歸0時,這個内部類型指針與shared_ptr管理的資源一起釋放
* 4)此外,為了保證線程安全,引用計數器的加1和減1都是原子操作,它保證了shared_ptr由多個線程共享時不會爆掉
#6  ===========================================================================================================================
一個典型的場景:
class A{
public:
     vector<classB> m_B_list;        //非常龐大的一個清單(實際這裡可以使vector指針,這裡隻是做一個描述)}
如果A有N多個執行個體,問題就出現了,每個A執行個體都要有一個m_B_list,首先很吃記憶體,其次各個A執行個體之間需要實施互相同步資料,進而保證這個
 vector對于所有A來說,都是一樣的。這是很棘手和難處理的:
 1)可以讓vector是static的,但是這樣比較占用static區域,而且無法釋放                       (不可選)
 2)可以簡單粗暴,讓所有A在更改vector之後,通知其他A跟新自己的vector,這樣既 占記憶體,又 占CPU         (不可選)
 3)建立一個command管理類,讓command管理類來存放vector,并提供增删改查接口給所有A使用,這樣保證了效率,一定程度上節約了記憶體。
    但是,需要新增一個類,而這個類的存在感很低,因為它隻是來做一個中轉                    (不是最優解)
 4)不建立command管理類,但是在外部區域建立vector,然後讓A都能通路,這樣有風險,如果某個A出于“某種原因”把vector給删掉了。
    那麼其他A在通路的時候将通路到空指針,或者直接崩潰。這種情況特别是在A都存放vector指針是明顯,因為析構時會釋放成員變量。(不是最優解)*5)使用shared_ptr,vector不使用new建立,而是使用shared_make,在A構造的時候建立。但是需要注意各個A運作在不同的線程下,同時
     操作vector可能産生競争。 第一種實作為 =============
    ---A.h---
     class A{
     public:
         A();    
         A(shared_ptr<vector<classB>> tmplist);     public:
         shared_ptr<vector<classB>> m_p_B_list;    }
    ---A.cpp---
     A::A():m_p_B_list(make_shared<vector<classB>>()){        //預設構造,清單初始化    }
     A::A(shared_ptr<vector<classB>> tmplist):m_p_B_list(tmplist){    }
(???)上面的實作雖然使用了共享指針管理動态記憶體,但是多個A如果都使用預設構造的話,實際上還是建立了多個vector執行個體,然後各個A
       管理自己的vector #7  ===========================================================================================================================
動态配置設定const對象是合法的,new const string("xxxx"); 
 按照const對象的屬性,const必須在定義是初始化,是以這種情況下不能使用預設構造函數。也可以通過delete來釋放配置設定的記憶體(???)疑問:const類型變量一般存放在 .data段 。那麼動态配置設定的常量是否 也在其中。如果不是,是否存放在堆内,如果是在堆内,那麼這塊
 區域是如何增加讀寫權限的,畢竟const類型隻讀不可寫。 #8  ===========================================================================================================================
記憶體耗盡:
 int *p  = new (nothrow) int;    //如果記憶體耗盡,那麼不抛異常bad_alloc,隻是傳回空指針注:預設情況下,c++中,記憶體耗盡會抛出異常bad_alloc
#9  ===========================================================================================================================
delete接收一個指針,指針要麼指向一個對象,要麼是空指針。
 delete動作會執行以下兩步:1)調用指針指向對象的析構函數;
               2)釋放指針指向的記憶體。 如果指針類型和指向的内容不一緻,比如 void* p = new classA();
 ====  例子  ==== 
 class A{
 public:
     A(){}
     ~A(){ cout << "~A()"; }
 };class B :public A{
 public:
     B(){}
     ~B(){ cout << "~B()"; }
 }; int _tmain(int argc, _TCHAR* argv[])
 {
     //A *p_A = new A();
     void *p_B = new B();    //delete p_A;
     delete p_B;    getchar();
    return 0;
 } 小結:1)delete隻認指針類型,如果指針是void類型,“那麼不會調用任何析構函數”。
       2)同理,如果有父子關系,那麼delete父類指針,“隻會調用父類的析構函數”。
 (*) 3)為了避免1)和2)中的問題,基類的析構函數必須是 “virtual” 的。
       4)不論delete接收的是子類的指針還是基類的指針。類執行個體都會被完整地釋放掉,delete是跟着執行個體走,隻認位址,而一旦涉及
      到位址部分,malloc和free便不再理會指針類型,而是從作業系統層面完成記憶體的釋放。     (!!!)注:上面說到了 “基類的析構函數必須是virtual,否則在delete指向子類的基類指針時,會出現子類析構不會
              被調用的問題”。如果子類中沒有需要回收的記憶體(通過new和malloc配置設定的),那麼記憶體是安全的,子類執行個體
              會被正确地釋放。但是如果子類中進行了動态記憶體配置設定(new,malloc),那麼這部分記憶體就無法再被回收,至此
              會導緻記憶體洩露。

 #10  ===========================================================================================================================
野指針:野指針是指,指針指向的記憶體已經被釋放掉了,但是指針沒有置空。使用delete操作指針并不會觸發指針的置空,是以在delete釋放
     完記憶體以後,需要手動把指針置空,因為記憶體區域已經不存在了,此時使用指針去通路就會觸發異常 ---> "非法記憶體區通路"。#11  ===========================================================================================================================
可以在delete後立即置空指針來避免野指針的出現。但是如果某塊記憶體區域被多個指針指向,那麼就很容易忽略某個指針的置空操作。
是以:但凡涉及到多個指針指向同一塊記憶體區域的場景,“   務必使用智能指針   ”來管理記憶體,此時我們不需要調用delete,隻需要管理指針
       即可,當所有的指針都置空,則記憶體被釋放。#12  ===========================================================================================================================
上面提到了智能指針的使用方法,通過make_shared來配置設定記憶體,但是如果想使用new來配置設定記憶體,然後交由智能指針管理,該如何操作???
首先,智能指針的構造函數時explict的,即不接受隐式轉換,即必須是指定類型,故先new在通過指派構造是行不通的。
可以使用複制構造函數來實作:
shared_ptr<classA> p_A = new classA();    //錯誤
 shared_ptr<classA> p_A(new classA());    //正确同理,使用普通指針給智能指針指派也是行不通的:
shared_ptr<classA> clone(){        //錯誤
     return new classA();        //傳回值不接收這種隐式轉換
 }shared_ptr<classA> clone(){                //正确
     return shared_ptr<classA>(new classA());    //傳回值也是智能指針
 } (!!!)注:上面提到的使用普通指針來初始化智能指針的場景,要求普通指針指向的必須是動态記憶體(即new配置設定的),靜态記憶體不行,
         比如:
                   int i = 10;
             const int j = 10;
             int *p = &i;
             const int *pj = &j;
             shared_ptr<int> sp(p);            //不能把棧給智能指針
             shared_ptr<int> spp(pj);        //不能把data區給智能指針#(!)13  ===========================================================================================================================
shared_ptr 和 unique_ptr 的構造和析構
 classA A;
 classA *p_A = &A;            //建立一個執行個體給普通指針unique_ptr<classA> up(new A());        //建立一個執行個體給unique智能指針
 shared_ptr<classA> sp(new A());    //建立一個執行個體給智能指針shared_ptr<classA> sp1(p_A);        //讓智能指針指向p_A(普通指針)指向的對象,要求unique_ptr指向的類型和shared_ptr指向的類型能夠互換
 shared_ptr<classA> sp2(up);        //讓智能指針指向unique_ptr指向的對象,要求unique_ptr指向的類型和shared_ptr指向的類型能夠互換shared_ptr<classA> sp3(p_A,mydelete)    //讓智能指針指向p_A(普通指針)指向的對象,在所有智能指針都釋放以後,不再使用delete釋放記憶體,而是使用mydelete
 shared_ptr<classA> sp4(sp1,mydelete)    //讓智能指針指向sp1(智能指針),并使用mydelete代替delete來釋放記憶體sp.reset()                //把sp從目前指向關系中解放出來,如果sp是最後一個指針,那麼調用delete釋放目标對象
 sp.reset(sp1)                //把sp重定向到sp1
 sp.reset(sp1,mydelete)            //把sp中定向到sp1,如果原sp指向的對象已經沒有指針再指向,那麼使用mydelete來釋放原對象     注:如果智能指針指向的不是new出來的動态記憶體,如果需要釋放資源,那麼一定要提供mydelete來替代delete,因為這種情況下不會調用delete
 #(!)14 ===========================================================================================================================
 (!!!)
 智能指針的使用:1)使用 普通指針/其他已存在的智能指針/其他已存在的普通指針,對目前建立的智能指針進行初始化。        (建立 指針 指向目前已有記憶體)
         2)使用 make_shared 建立全新的記憶體區,然後建立一個全新的智能指針指向它。                (建立 記憶體空間 和 指針)
         3)使用 智能指針的 reset 方法來進行 智能指針的 重定向 和 釋放                        (重定向 和 銷毀)#15   ===========================================================================================================================
weak_ptr
weak_ptr 是給 shared_ptr做補充的,把weak_ptr增加到shared_ptr指向的對象上,不會增加計數,單當shared_ptr全部釋放完以後,
 weak_ptr指向的内容也将不存在。 weak_ptr的使用必須 經過自己的lock()方法,這個方法會peek對象是否存在,存在傳回true,否則
 false,用完釋放weak_ptr亦不會導緻計數減少。weak_prt可以了解為shared_ptr的螢幕。僅僅用來peek資料。