C++ 引用計數技術及智能指針的簡單實現

文章也發佈在 騰訊雲+社區程序員

一直以來都對智能指針只知其一;不知其二,看C++Primer中也講的不夠清晰明白(大概是我功力不夠吧)。最近花了點時間認真看了智能指針,特意來寫這篇文章。安全

1.智能指針是什麼

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

2.普通指針存在的問題

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已經被刪除了。這個過程是這樣的:
this

從圖能夠看出,錯誤的產生來自於ptr1的」無知「:它並不知道還有其餘指針共享着它指向的對象。若是有個辦法讓ptr1知道,除了它本身外還有兩個指針指向基礎對象,而它不該該刪除基礎對象,那麼懸垂指針的問題就得以解決了。以下圖:
設計

那麼什麼時候才能夠刪除基礎對象呢?固然是隻有一個指針指向基礎對象的時候,這時經過該指針就能夠大大方方地把基礎對象刪除了。指針

3.什麼是引用計數

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

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

4.智能指針實現

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

4.1.基礎對象類

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

class Point                                       
{
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; }

private:
    int x, y;

};

4.2.輔助類

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

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

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

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

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

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

class SmartPtr
{
public:
    SmartPtr(Point *ptr) :rp(new U_Ptr(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;  
};

4.4.智能指針類的使用與測試

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

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()已經獲取不到原來的值。

5.智能指針類的改進一

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

{
public:
    SmartPtr(Point *ptr) :rp(new U_Ptr(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:
    U_Ptr *rp;  
};

而後咱們能夠像指針般使用智能指針類

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

6.智能指針改進二

目前這個智能指針智能用於管理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個指針指向基礎對象
        請按任意鍵繼續. . .
相關文章
相關標籤/搜索