[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  ===========================================================================================================================

https://www.cnblogs.com/yinbiao/p/11563520.html

引用計數法的內部實現:

  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),那麼這部分內存就無法再被回收,至此

              會導致內存泄露。

    詳細描述:https://blog.csdn.net/u013797029/article/details/41848407


#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數據。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章