1. 程式人生 > >C++ 引用計數技術及智慧指標的簡單實現

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

一直以來都對智慧指標一知半解,看C++Primer中也講的不夠清晰明白(大概是我功力不夠吧)。最近花了點時間認真看了智慧指標,特地來寫這篇文章。

1.智慧指標是什麼

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

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

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

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

3.什麼是引用計數

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

  • 簡化跟蹤堆中(也即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個指標指向基礎物件
        請按任意鍵繼續. . .

相關推薦

C++ 引用計數技術智慧指標簡單實現

一直以來都對智慧指標一知半解,看C++Primer中也講的不夠清晰明白(大概是我功力不夠吧)。最近花了點時間認真看了智慧指標,特地來寫這篇文章。 1.智慧指標是什麼 簡單來說,智慧指標是一個類,它對普通指標進行封裝,使智慧指標類物件具有普通指標型別一樣的操作。具體而言,複製物件時,副本和原物件都指向同一儲存

C++智慧指標簡單實現

#include <iostream> #include <string> #include <vector> #include <list> namespace smart_pointer { // RAII(Resou

C++智慧指標簡單剖析

導讀 《C++ Primer Plus》第六版,其中關於智慧指標的章節解析的非常清晰。C++面試過程中,很多面試官都喜歡問智慧指標相關的 問題,比如你知道哪些智慧指標?shared_ptr的設計原理是什麼?如果讓你自己設計一個智慧指標,你如何完成?等等……。而且在看開源的C

C++引用計數(reference counting)技術簡介(3)

1.將Reference Counting加到既有的Class 要想將引用計數施加到現有的實值物件Widget上,按照前面討論的,都需要修改Winget類的原始碼。但是,有時程式庫的內容不是我們呢可以修改的,又該如何做呢? 如果令Widget繼承自RCObj

C++引用計數(reference counting)技術簡介(1)

1.引用計數的作用 C++引用計數是C++為彌補沒有垃圾回收機制而提出的記憶體管理的一個方法和技巧,它允許多個擁有共同值的物件共享同一個物件實體。 C++的引用計數作為記憶體管理的方法和技術手段主要有一下兩個作用。 (1)簡化了堆物件(Heap Objec

C++11】4種智慧指標

|auto_ptr(不要使用的指標) 沒有智慧指標的c++時代,對堆記憶體的管理就是簡單的new delete。 但是缺點是容易忘了delete釋放,即使是資深碼農,也可能會在某一個地方忘記delete它,造成記憶體洩漏。 在實際工程中,我們往往更希望把精力放在應用層上,而不是費盡心思在語言的細

C++Boost庫學習之智慧指標 shared_ptr

目錄 1.共享指標shared_ptr ^   使用例子 ^ #include<boost/shared_ptr.hpp> using namespace boost; using std::cout; using std::endl; str

詳解c++ 引用(reference)與 指標(pointer)的區別與聯絡

引用(reference) 與指標(pointer)之間有什麼區別和聯絡呢?我相信,對於很多初學c++的程式猿來說,不是一件簡單的事.那麼在c++中,引用與指標到底有什麼聯絡和區別呢?要弄清楚這個問題,我們必須明白 : 1.什麼是引用? 2.怎樣使用引用?

C++過載——智慧指標實現

本文參照於狄泰軟體學院,唐佐林老師的——《C++深度剖析教程》 智慧指標背後的設計思想 在專案開發中,有一個臭名昭著的BUG——記憶體洩漏。不管是新手還是老手都容易犯這個錯誤。並且這種BUG很難查詢。 1. 編譯器並不能發現記憶體洩漏的問題。 2

c++智慧指標實現

智慧指標(smart pointer)是儲存指向動態分配(堆)物件指標的類,用於生存期控制,能夠確保自動正確的銷燬動態分配的物件,防止懸垂指標的出現。在類物件中,如果定 義一個int型的指標,當多次使用同一個已存在的物件初始化一些新建物件時,已存在的物件和新建的物件都指向同

[C/C++]_[中級]_[使用智慧指標的方式釋放malloc出來的堆空間]

場景:1. 使用auto_ptr 的方式可以wrap類物件,  之後在方法結束後可以自動釋放物件, 參考;這樣在有條件判斷的語句時可以省掉free語句或CloseHandle.2.C++的特性之一就是類物件(非返回值的物件)在方法結束後會自動呼叫解構函式,這樣在解構函式裡可以

C++利用智慧指標shared_ptr實現物件池

         C++中用new來分配物件,還要顯式的呼叫delete來析構物件,很容易造成記憶體洩露。所以在研究我們遊戲伺服器的程式碼的時候發現,我們將new函式封裝,在物件池上,利用shared_ptr的特性來自動釋放物件,同時實現了一種簡單的GC回收物件的機制。看完

C++學習筆記】詳解C++中的三種智慧指標

一、簡介 由於 C++ 語言沒有垃圾回收機制,程式設計師每次 new出來的記憶體都要手動 delete。程式設計師忘記 delete,有可能就會造成記憶體洩漏,程式崩潰等嚴重的後果。用智慧指標便可以有效緩解這類問題,本文主要講解常見的智慧指標的用法。包括:s

C++11新特性之智慧指標

  這一節將從用法上、記憶體儲存上以及生存週期上,對unique_ptr, shared_ptr和weak_ptr做一個深入剖析。 unique_ptr   不共享它的指標。它無法複製到其他 unique_ptr,無法通過值傳遞到函式,也無法用於需要副本的

簡單智慧指標實現

直上程式碼,大佬忽略 #include <iostream> #include <string> using namespace std; //定義智慧指標模板類 template<typename T> class SmartP

一個簡單智慧指標實現

#include<iostream> #include<memory> using namespace std; template<typename T> cla

C++深度探索系列:智慧指標(Smart Pointer) [二]

                                           深度探索智慧指標(Smart Pointer) 主題索引: 一、剖析C++標準庫智慧指標(std::auto_ptr)        1.Do you Smart Pointer?    2

Swift 引用計數總結 Strong,Weak, unowned 簡單使用

每天一小結,必須讀幾篇部落格在閒暇時,下面開始進入正題: ARC ARC 蘋果版本的自動記憶體管理的編譯時間特性。它代表了自動引用計數(Automatic Reference Counting)。也就是對於一個物件來說,只有在引用計數為0的情況下記憶體才會被釋放。

C++中的三種智慧指標分析(RAII思想)

智慧指標 首先我們在理解智慧指標之前我們先了解一下什麼是RAII思想。RAII(Resource Acquisition Is I

BT Tracker的原理.Net Core簡單實現Tracker Server

最近很忙,自上次Blog被盜 帖子全部丟失後也很少時間更新Blog了,閒暇在外國站點查閱資料正好看到一些Tracker 的協議資料,也就今天記錄並實踐了下,再次分享給大家希望可以幫到需要的小夥伴。 首先我們來了解下BT Tracker 一、做種       現在