C++智能指針實現和問題

Share_ptr實現原理

智能指針是什麼

簡單來說,智能指針是一個類,它對普通指針進行封裝,使智能指針類對象具有普通指針類型一樣的操作。具體而言,複製對象時,副本和原對象都指向同一存儲區域,如果通過一個副本改變其所指的值,則通過另一對象訪問的值也會改變.所不同的是,智能指針能夠對內存進行進行自動管理,避免出現懸垂指針等情況。

普通指針存在的問題

C語言、C++語言沒有自動內存回收機制,關於內存的操作的安全性依賴於程序員的自覺。程序員每次new出來的內存塊都需要自己使用delete進行釋放,流程複雜可能會導致忘記釋放內存而造成內存泄漏。而智能指針也致力於解決這種問題,使程序員專注於指針的使用而把內存管理交給智能指針。

我們先來看看普通指針的懸垂指針問題。當有多個指針指向同一個基礎對象時,如果某個指針delete了該基礎對象,對這個指針來說它是明確了它所指的對象被釋放掉了,所以它不會再對所指對象進行操作,但是對於剩下的其他指針來說呢?它們還傻傻地指向已經被刪除的基礎對象並隨時準備對它進行操作。於是懸垂指針就形成了,程序崩潰也“指日可待”。我們通過代碼+圖來來探求懸垂指針的解決方法。

int * ptr1 = new int (1);
int * ptr2 = ptr1;
int * ptr3 = prt2;
        
cout << *ptr1 << endl;
cout << *ptr2 << endl;
cout << *ptr3 << endl;

delete ptr1;

cout << *ptr2 << endl;

代碼簡單就不囉嗦解釋了。運行結果是輸出ptr2時並不是期待的1,因爲1已經被刪除了。這個過程是這樣的:

從圖可以看出,錯誤的產生來自於ptr1的”無知“:它並不知道還有其他指針共享着它指向的對象。如果有個辦法讓ptr1知道,除了它自己外還有兩個指針指向基礎對象,而它不應該刪除基礎對象,那麼懸垂指針的問題就得以解決了。如下圖:

那麼何時纔可以刪除基礎對象呢?當然是只有一個指針指向基礎對象的時候,這時通過該指針就可以大大方方地把基礎對象刪除了。

什麼是引用計數

如何來讓指針知道還有其他指針的存在呢?這個時候我們該引入引用計數的概念了。引用計數是這樣一個技巧,它允許有多個相同值的對象共享這個值的實現。引用計數的使用常有兩個目的:

  • 簡化跟蹤堆中(也即C++中new出來的)的對象的過程。一旦一個對象通過調用new被分配出來,記錄誰擁有這個對象是很重要的,因爲其所有者要負責對它進行delete。但是對象所有者可以有多個,且所有權能夠被傳遞,這就使得內存跟蹤變得困難。引用計數可以跟蹤對象所有權,並能夠自動銷燬對象。可以說引用計數三個簡單的垃圾回收體系。這也是本文的討論重點。
  • 節省內存,提高程序運行效率。如何很多對象有相同的值,爲這多個相同的值存儲多個副本是很浪費空間的,所以最好做法是讓左右對象都共享同一個值的實現。C++標準庫中string類採取一種稱爲”寫時複製“的技術,使得只有當字符串被修改的時候才創建各自的拷貝,否則可能(標準庫允許使用但沒強制要求)採用引用計數技術來管理共享對象的多個對象。這不是本文的討論範圍。

智能指針實現

瞭解了引用計數,我們可以使用它來寫我們的智能指針類了。智能指針的實現策略有兩種:輔助類與句柄類。這裏介紹輔助類的實現方法。

基礎對象類

首先,我們來定義一個基礎對象類Point類,爲了方便後面我們驗證智能指針是否有效,我們爲Point類創建如下接口:

class Point                                       
{
private:
    int x, y;
public:
    Point(int xVal = 0, int yVal = 0) :x(xVal), y(yVal) { }
    int getX() const { return x; }
    int getY() const { return y; }
    void setX(int xVal) { x = xVal; }
    void setY(int yVal) { y = yVal; }
};

輔助類

在創建智能指針類之前,我們先創建一個輔助類。這個類的所有成員皆爲私有類型,因爲它不被普通用戶所使用。爲了只爲智能指針使用,還需要把智能指針類聲明爲輔助類的友元。這個輔助類含有兩個數據成員:計數count與基礎對象指針。也即輔助類用以封裝使用計數與基礎對象指針

class U_Ptr                                  
{
private:
    
    friend class SmartPtr;      
    U_Ptr(Point *ptr) :p(ptr), count(1) { }
    ~U_Ptr() { delete p; }
    
    int count;   
    Point *p;                                                      
};

爲基礎對象類實現智能指針類

引用計數是實現智能指針的一種通用方法。智能指針將一個計數器與類指向的對象相關聯,引用計數跟蹤共有多少個類對象共享同一指針。它的具體做法如下:

  • 當創建類的新對象時,初始化指針,並將引用計數設置爲1
  • 當對象作爲另一個對象的副本時,複製構造函數複製副本指針,並增加與指針相應的引用計數(加1)
  • 使用賦值操作符對一個對象進行賦值時,處理複雜一點:先使左操作數的指針的引用計數減1(爲何減1:因爲指針已經指向別的地方),如果減1後引用計數爲0,則釋放指針所指對象內存。然後增加右操作數所指對象的引用計數(爲何增加:因爲此時做操作數指向對象即右操作數指向對象)。
  • 析構函數:調用析構函數時,析構函數先使引用計數減1,如果減至0則delete對象。

做好前面的準備後,我們可以來爲基礎對象類Point書寫一個智能指針類了。根據引用計數實現關鍵點,我們可以寫出我們的智能指針類如下:

{
public:
    SmartPtr(Point *ptr) :rp(new RefPtr(ptr)) { }    
    
    SmartPtr(const SmartPtr &sp) :rp(sp.rp) { ++rp->count; }
      
    SmartPtr& operator=(const SmartPtr& rhs) {    
        ++rhs.rp->count;    
        if (--rp->count == 0)    
            delete rp;
        rp = rhs.rp;
        return *this;
    }
    
    ~SmartPtr() {       
        if (--rp->count == 0)   
            delete rp;
        else 
        cout << "還有" << rp->count << "個指針指向基礎對象" << endl;
    }
    
private:
    U_Ptr *rp;  
};

智能指針類的使用與測試

至此,我們的智能指針類就完成了,我們可以來看看如何使用

int main()
{
    //定義一個基礎對象類指針
    Point *pa = new Point(10, 20);

    //定義三個智能指針類對象,對象都指向基礎類對象pa
    //使用花括號控制三個指針指針的生命期,觀察計數的變化

    {
        SmartPtr sptr1(pa);//此時計數count=1
        {
            SmartPtr sptr2(sptr1); //調用複製構造函數,此時計數爲count=2
            {
                SmartPtr sptr3=sptr1; //調用賦值操作符,此時計數爲conut=3
            }
            //此時count=2
        }
        //此時count=1;
    }
    //此時count=0;pa對象被delete掉

    cout << pa->getX ()<< endl;

    system("pause");
    return 0;
}

來看看運行結果咯:

還有2個指針指向基礎對象
還有1個指針指向基礎對象
-17891602
請按任意鍵繼續. . .

如期,在離開大括號後,共享基礎對象的指針從3->2->1->0變換,最後計數爲0時,pa對象被delete,此時使用getX()已經獲取不到原來的值。

智能指針類的改進一

雖然我們的SmartPtr類稱爲智能指針,但它目前並不能像真正的指針那樣有->、*等操作符,爲了使它看起來更像一個指針,我們來爲它重載這些操作符。代碼如下所示:

{
public:
    SmartPtr(Point *ptr) :rp(new RefPtr(ptr)) { }    
    
    SmartPtr(const SmartPtr &sp) :rp(sp.rp) { ++rp->count; }
      
    SmartPtr& operator=(const SmartPtr& rhs) {    
        ++rhs.rp->count;    
        if (--rp->count == 0)    
            delete rp;
        rp = rhs.rp;
        return *this;
    }
    
    ~SmartPtr() {       
        if (--rp->count == 0)   
            delete rp;
        else 
        cout << "還有" << rp->count << "個指針指向基礎對象" << endl;
    }
    

    Point & operator *()        //重載*操作符  
    {
        return *(rp->p);
    }
    Point* operator ->()       //重載->操作符  
    {
        return rp->p;
    }
    
private:
    RefPtr *rp;  
};

然後我們可以像指針般使用智能指針類

  Point *pa = new Point(10, 20);
    SmartPtr sptr1(pa);
    //像指針般使用
    cout<<sptr1->getX();

智能指針改進二

目前這個智能指針智能用於管理Point類的基礎對象,如果此時定義了個矩陣的基礎對象類,那不是還得重新寫一個屬於矩陣類的智能指針類嗎?但是矩陣類的智能指針類設計思想和Point類一樣啊,就不能借用嗎?答案當然是能,那就是使用模板技術。爲了使我們的智能指針適用於更多的基礎對象類,我們有必要把智能指針類通過模板來實現。這裏貼上上面的智能指針類的模板版:

    //模板類作爲友元時要先有聲明
    template <typename T>
    class SmartPtr;
    
    template <typename T>
    class U_Ptr     //輔助類
    {
    private:
        //該類成員訪問權限全部爲private,因爲不想讓用戶直接使用該類
        friend class SmartPtr<T>;      //定義智能指針類爲友元,因爲智能指針類需要直接操縱輔助類
    
        //構造函數的參數爲基礎對象的指針
        U_Ptr(T *ptr) :p(ptr), count(1) { }
    
        //析構函數
        ~U_Ptr() { delete p; }
        //引用計數
        int count;   
    
        //基礎對象指針
        T *p;                                                      
    };
    
    template <typename T>
    class SmartPtr   //智能指針類
    {
    public:
        SmartPtr(T *ptr) :rp(new U_Ptr<T>(ptr)) { }      //構造函數
        SmartPtr(const SmartPtr<T> &sp) :rp(sp.rp) { ++rp->count; }  //複製構造函數
        SmartPtr& operator=(const SmartPtr<T>& rhs) {    //重載賦值操作符
            ++rhs.rp->count;     //首先將右操作數引用計數加1,
            if (--rp->count == 0)     //然後將引用計數減1,可以應對自賦值
                delete rp;
            rp = rhs.rp;
            return *this;
        }
    
        T & operator *()        //重載*操作符  
        {
            return *(rp->p);
        }
        T* operator ->()       //重載->操作符  
        {
            return rp->p;
        }
    
    
        ~SmartPtr() {        //析構函數
            if (--rp->count == 0)    //當引用計數減爲0時,刪除輔助類對象指針,從而刪除基礎對象
                delete rp;
            else 
            cout << "還有" << rp->count << "個指針指向基礎對象" << endl;
        }
    private:
        U_Ptr<T> *rp;  //輔助類對象指針
    };
    
    

好啦,現在我們能夠使用這個智能指針類對象來共享其他類型的基礎對象啦,比如int:

int main()
{
    int *i = new int(2);
    {
        SmartPtr<int> ptr1(i);
        {
            SmartPtr<int> ptr2(ptr1);
            {
                SmartPtr<int> ptr3 = ptr2;

                cout << *ptr1 << endl;
                *ptr1 = 20;
                cout << *ptr2 << endl;

            }
        }
    }
    system("pause");
    return 0;
}

運行結果如期所願,SmartPtr類管理起int類型來了:

        2
        20
        還有2個指針指向基礎對象
        還有1個指針指向基礎對象
        請按任意鍵繼續. . .

 

Share_Ptr使用避坑條款:

條款1:不要把一個原生指針給多個shared_ptr管理


int* ptr = new int;
shared_ptr<int> p1(ptr);
shared_ptr<int> p2(ptr); //logicerror
ptr對象被刪除了2次
這種問題比喻成“二龍治水”,在原生指針中也同樣可能發生。

條款2:不要把this指針給shared_ptr


class Test{
public:
    void Do(){ m_sp = shared_ptr<Test>(this); }
private:
   shared_ptr<Test> m_member_sp;
};

Test* t = new Test;
shared_ptr<Test>local_sp(t);
p->Do();

發生什麼事呢,t對象被刪除了2次!
t對象給了local_sp管理,然後在m_sp = shared_ptr<Test>(this)這句裏又請了一尊神來管理t。
這就發生了條款1裏“二龍治水”錯誤。

條款3:shared_ptr作爲被保護的對象的成員時,小心因循環引用造成無法釋放資源。

對象需要相互協作,對象A需要知道對象B的地址,這樣才能給對象B發消息(或調用其方法)。
設計模式中有大量例子,一個對象中有其他對象的指針。現在把原生指針替換爲shared_ptr.

假設a對象中含有一個shared_ptr<B>指向b對象;假設b對象中含有一個shared_ptr<A>指向a對象
並且a,b對象都是堆中分配的。很輕易就能與他們失去最後聯繫。
考慮某個shared_ptr<A>local_a;是我們能最後一個看到a對象的共享智能指針,其use_count==2,
因爲對象b中持有a的指針。所以當local_a說再見時,local_a只是把a對象的use_count改成1。
同理b對象。然後我們再也看不到a,b的影子了,他們就靜靜的躺在堆裏,成爲斷線的風箏。

解決方案是:Use weak_ptr to "break cycles."(boost文檔裏寫的)或者顯示的清理

條款4:不要在函數實參裏創建shared_ptr

function ( shared_ptr<int>(newint), g( ) );  //有缺陷
可能的過程是先new int,然後調g( ),g()發生異常,shared_ptr<int>沒有創建,int內存泄露

shared_ptr<int> p(newint());
f(p, g());  //Boost推薦寫法

條款5:對象內部生成shared_ptr

前面說過,不能把this指針直接扔給shared_ptr. 但是沒有禁止在對象內部生成自己的shared_ptr

//這是Boost的例子改的。
class Y: publicboost::enable_shared_from_this<Y>
{
   boost::shared_ptr<Y> GetSelf()
    {
       return shared_from_this();
    }
};

原理是這樣的。普通的(沒有繼承enable_shared_from_this)類T的shared_ptr<T>p(new T).
p作爲棧對象佔8個字節,爲了記錄(new T)對象的引用計數,p會在堆上分配16個字節以保存
引用計數等“智能信息”。share_ptr沒有“嵌入(intrusive)”到T對象,或者說T對象對share_ptr毫不知

情。Y對象則不同,Y對象已經被“嵌入”了一些share_ptr相關的信息,目的是爲了找到“全局性”的
那16字節的本對象的“智能信息”。

原理說完了,就是陷阱
Y y;
boost::shared_ptr<Y> p= y.GetSelf(); //無知的代碼,y根本就不是new出來的

Y* y = new Y;
boost::shared_ptr<Y> p= y->GetSelf(); //似是而非,仍舊程序崩盤。
Boost文檔說,在調用shared_from_this()之前,必須存在一個正常途徑創建的shared_ptr

boost::shared_ptr<Y> spy(newY)
boost::shared_ptr<Y> p = spy->GetSelf(); //OK

條款6 :處理不是new的對象要小心。

int* pi = (int*)malloc(4)
shared_ptr<int> sp( pi ) ;//delete馬嘴不對malloc驢頭。

條款7:多線程對引用計數的影響。

如果是輕量級的鎖,比如InterLockIncrement等,對程序影響不大
如果是重量級的鎖,就要考慮因爲share_ptr維護引用計數而造成的上下文切換開銷。
1.33版本以後的shared_ptr對引用計數的操作使用的是Lock-Free(類似InterLockIncrement函數族)
的操作,應該效率不錯,而且能保證線程安全(庫必須保證其安全,程序員都沒有干預這些隱藏事物的機會)。
Boost文檔說read,write同時對shared_ptr操作時,行爲不確定。這是因爲shared_ptr本身有兩個成員px,pi。
多線程同時對px讀寫是要出問題的。與一個int的全局變量多線程讀寫會出問題的原因一樣。

條款8:對象數組用shared_array

int* pint = new int[100];
shared_array<int> p (pint);

既然shared_ptr對應着delete;顯然需要一個delete[]對應物shared_array

條款9:學會用刪除器

struct Test_Deleter
{   
    void operator ()( Test* p){  ::free(p);   }
};
Test* t = (Test*)malloc(sizeof(Test));
new (t) Test;

shared_ptr<Test> sp( t , Test_Deleter() ); //刪除器可以改變share_ptr銷燬對象行爲

有了刪除器,shared_array無用武之地了。
template<class T>
struct Array_Deleter
{   
    void operator ()( T*){   delete[] p;  }
};
int* pint = new int[100];
shared_ptr<int> p (pint,Array_Deleter<int>() );

條款10:學會用分配器

存放引用計數的地方是堆內存,需要16-20字節的開銷。
如果大量使用shared_ptr會造成大量內存碎片。
shared_ptr構造函數的第3個參數是分配器,可以解決這個問題。

shared_ptr<Test> p( (new Test),Test_Deleter(), Mallocator<Test>());
注意刪除器Test_Deleter是針對Test類的。分配器是針對shared_ptr內部數據的。

Mallocator<Test>()是個臨時對象(無狀態的),符合STL分配器規約。

template <typenameT> 
class Mallocator { 
    //略。。。。。。
    T * allocate(constsize_t n) const {
       returnsingleton_pool<T,sizeof(T)>::malloc();
    }
    //略。。。。。。

Mallocator傳入Test,實際分配的類型確是
class boost::detail::sp_counted_impl_pda<classTest *,
                                   structTest_Deleter,
                                   classMallocator<class Test>>
這是用typeid(T).name()打印出來的。可能和rebind相關。

條款11 weak_ptr在使用前需要檢查合法性。


weak_ptr<K> wp;
{
shared_ptr<K> sp(new K); //sp.use_count()==1
wp = sp; //wp不會改變引用計數,所以sp.use_count()==1
shared_ptr<K> sp_ok = wp.lock();//wp沒有重載->操作符。只能這樣取所指向的對象
}
shared_ptr<K> sp_null =wp.lock(); //sp_null .use_count()==0;
因爲上述代碼中sp和sp_ok離開了作用域,其容納的K對象已經被釋放了。
得到了一個容納NULL指針的sp_null對象。在使用wp前需要調用wp.expired()函數判斷一下。
因爲wp還仍舊存在,雖然引用計數等於0,仍有某處“全局”性的存儲塊保存着這個計數信息。
直到最後一個weak_ptr對象被析構,這塊“堆”存儲塊才能被回收。否則weak_ptr無法直到自己
所容納的那個指針資源的當前狀態。

條款12 不要newshared_ptr<T>

本來shared_ptr就是爲了管理指針資源的,不要又引入一個需要管理的指針資源shared_ptr<T>*

條款13  儘量不要get

class B{...};
class D : public B{ ...};  //繼承層次關係

shared_ptr<B> sp (new D);   //通過隱式轉換,儲存D的指針。
B* b = sp.get();           //shared_ptr辛辛苦苦隱藏的原生指針就這麼被刨出來了。
D* d = dynamic_cast<D*>(b); //這是使用get的正當理由嗎?

正確的做法
shared_ptr<B> spb (new D) ;
shared_ptr<D> spd =shared_dynamic_cast<D>(spb);//變成子類的指針
shared_ptr在竭盡全力表演的像一個原生指針,原生指針能幹的事,它也基本上能幹。

另一個同get相關的錯誤
shared_ptr<T> sp(new T);
shared_ptr<T> sp2( sp.get() );//又一個“二龍治水”實例,指針會刪2次而錯誤。

條款14 不要memcpy shared_ptr

shared_ptr<B> sp1 (new B) ;
shared_ptr<B> sp2;
memcpy(&sp2,&sp1,sizeof(shared_ptr<B>));//sp2.use_count()==1
很顯然,不是通過正常途徑(拷貝構造,賦值運算),引用計數是不會正確增長的。

條款15 使用BOOST預定義的宏去改變shared_ptr行爲。

shared_ptr行爲由類似BOOST_SP_DISABLE_THREADS這樣的宏控制。需要去學習他們到底是幹什麼的。
大師Andrei Alexandrescu設計了一種基於模板策略設計模式的智能指針,通過幾個模板參數去定製化
智能指針的行爲。Boost卻不以爲然,官方解釋是:需要統一的接口,這樣利於大規模書寫。
smart_ptr<T,OwnershipPolicy,ConversionPolicy,CheckingPolicy,StoragePolicy>sp(new T);
上述接口缺點是外形複雜,看上去像個大花臉。優點是客戶程序員可以輕易的定製行爲。

條款17 構造函數裏調用shared_from_this拋例外

class Holder:publicenable_shared_from_this<Holder>{
public:
    Holder() {
       shared_ptr<Holder>sp = shared_from_this();
       int x = sp.use_count();
    }
};
同前面條款5,不符合enable_shared_from_this使用前提。

原文鏈接:https://blog.csdn.net/NicolasYan/article/details/50588022
原文鏈接:https://blog.csdn.net/peng864534630/article/details/77932574

 

 

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