1. 程式人生 > >C++:智慧指標

C++:智慧指標

首先,為什麼需要智慧指標?

來看下面這段程式碼

bool doSomething()
{

    // 如果時間執行失敗了就返回false
    return false;
}

// 為了避免記憶體洩漏和檔案描述符洩漏,我們需要寫出以下這樣冗餘的程式碼
// 我們需要一種方法讓他自動的釋放掉
void Test1()
{
    int* p1 = new int[2];
    FILE* pf = fopen("test","r");
    // 1.如果開啟檔案失敗,就需要釋放p1的空間
    if(pf == NULL)
    {
        delete[] p1;
    }

    // 2.如果執行事件失敗,就需要釋放p1的空間,並且關閉pf檔案
if(!doSomething()) { delete[] p1; fclose(pf); return; } // 3.如果丟擲了異常,我們捕獲異常以後,也需要釋放p1的空間,並且關閉檔案描述符 try { throw 1; } catch(int err) { delete[] p1; fclose(pf); return; } // 4.邏輯正常結束,也需要釋放空間和關閉檔案 delete
[] p1; fclose(pf); return; }

可以看到,在這段程式碼裡,我們需要考慮一些異常情況,並且要非常非常注意釋放資源。但是造成的問題就是,程式碼冗餘,反覆重複的釋放程式碼,降低了可讀性,並且反覆多次的釋放,如果有哪一個忘記寫了或是漏寫了,就會造成記憶體洩漏。

所以,這就體現出了智慧指標的好處。

什麼是智慧指標

一個智慧指標應具備以下的三點:

  • RAII
  • 像指標一樣的使用
  • 拷貝構造與賦值
    先來說第一點:RAII
    後面兩點後面會說

RAII

資源分配即初始化,定義一個類來封裝資源的分配與釋放,在建構函式完成資源的分配與初始化,在解構函式完成資源的清理,可以保證資源的正確初始化和釋放。

再來看看都有哪些智慧指標

auto_ptr

該智慧指標是c98標準裡規定的,需要注意的是,這個智慧指標是不提倡使用的,某些情況下禁止使用的。所以只做瞭解。
上文種提到了智慧指標的三點,再次討論一下auto_ptr的後兩點:
1.像指標一樣使用,很容易就可以使用,過載*和->就可以實現
2.拷貝構造與賦值:拷貝構造預設的拷貝方式是淺拷貝,即把你的拷一份給我,對指標而言,就是你和我都指向同一空間,那麼就會出現很大的問題,析構時會析構兩次。auto_ptr對於該問題的解決方案是使用管理權轉移的思想
這裡寫圖片描述

我們來模擬實現一個auto_ptr:

template <class T>
class Auto_Ptr{
public:
    Auto_Ptr(T* ptr)
        :_ptr(ptr)
    {}
    ~Auto_Ptr()
    {
        if(_ptr!=NULL)
        {
            cout<<"delete _ptr"<<endl;
            delete _ptr;
        }
    }
    T& operator*()
    {
        return *_ptr;
    }

    T* operator->()
    {
        return _ptr;
    }
    //以下做了管理權轉移
    Auto_Ptr(Auto_Ptr<T>& ap)
        :_ptr(ap._ptr)
    {
        ap._ptr=NULL;
    }
    //ap3=ap2;
    T& operator=(Auto_Ptr<T>& ap)
    {
        if(this!=&ap)
        {
            if(_ptr!=NULL)
            {
                delete _ptr;
            }
            _ptr=ap._ptr;
            ap._ptr=NULL;
        }
        return *this;
    }

private:
    T* _ptr;
};

scoped_ptr

以下智慧指標都是針對於Boots庫中的智慧指標。
與auot_ptr相似,scoped_ptr也具有以上三點,與auto_ptr不同的是對於拷貝構造與賦值實現思想不同。
scoped_ptr採用了防拷貝的思想,意思是,如果你要拷貝或是賦值但是我不允許這種行為。如果採取預設的拷貝構造與賦值,預設是淺拷貝,也就意味著我們必須實現一個,但是如何實現呢?scoped_ptr採取的方法是隻宣告不實現。並且將介面設定為私有的,也就說明你想實現也實現不了。

template <class T>
class Scoped_Ptr
{
public:
    Scoped_Ptr(T* ptr)
        :_ptr(ptr)
    {}
    ~Scoped_Ptr()
    {
        delete _ptr;
    }
    T& operator*()
    {
        return *_ptr;
    }
    T* operator->()
    {
        return _ptr;
    }
private:
    //防拷貝
    //只宣告不實現
    //定義為私有的,也不允許你在外面實現
    Scoped_Ptr(const Scoped_Ptr<T>& sp);
    Scoped_Ptr<T>& operator=(const Scoped_Ptr<T>& sp);
private:
    T* _ptr;
};

shared_ptr

shared_ptr使用了一種我們比較熟悉的做法:引用計數
這裡寫圖片描述

template<class T>
class Shared_Ptr
{
public:
    Shared_Ptr(T* ptr)
        :_ptr(ptr)
    {
        _pcount=new int(1);
    }
    ~Shared_Ptr()
    {
        if(--(*_pcount)==0)
        {
            delete _ptr;
            delete _pcount;
        }
    }
    Shared_Ptr(const Shared_Ptr& sp)
        :_ptr(sp._ptr)
         ,_pcount(sp._pcount)
    {
        ++(*_pcount);
    }
    //sp1=sp2;
    Shared_Ptr<T>& operator=(const Shared_Ptr<T>& sp)
    {
        if(_ptr!=sp._ptr)
        {
            if(--(*_pcount)==0)
            {
                delete _pcount;
                delete _ptr;
            }
            _ptr=sp._ptr;
            _pcount=sp._pcount;
            ++(*_pcount);
        }
        return *this;
    }
    T& operator*()
    {
        return *_ptr;
    }

    T* operator->()
    {
        return _ptr;
    }
private:
    T* _ptr;
    int* _pcount;

};

這種做法乍一看是很完美,但是shared_ptr有一些問題:
1. 執行緒安全問題:由於對引用計數++不是原子的,所以這裡是有執行緒安全問題的
2. 迴圈引用問題:這個問題使用以上問題是不會有問題的,比如在以下場景:

struct ListNode
{
    Shared_Ptr<ListNode> _next;
    Shared_Ptr<ListNode> _prev;


    ListNode()
        :_next(NULL)
         ,_prev(NULL)
    {}

    ~ListNode()
    {
        cout<<"~ListNode()"<<endl;
    }
};

void TestCycle()
{
    Shared_Ptr<ListNode> cur(new ListNode);
    Shared_Ptr<ListNode> next(new ListNode);

    cur->_next = next;
    next->_prev = cur;
}

這裡寫圖片描述

那麼該如何解決這個問題,我們不妨思考一下,造成的原因是由於引用計數,那麼我們就不要讓引用計數自增就好了。所以就需要另外一個智慧指標出場–weak_ptr

weak_ptr

這個智慧指標不含RAII,因為他的作用就是為了解決shared_ptr的迴圈引用問題的。
如何解決呢?看程式碼:

struct ListNode
{
    Weak_Ptr<ListNode> _next;
    Weak_Ptr<ListNode> _prev;


    ListNode()
        :_next(NULL)
         ,_prev(NULL)
    {}

    ~ListNode()
    {
        cout<<"~ListNode()"<<endl;
    }
};

void TestCycle()
{
    Shared_Ptr<ListNode> cur(new ListNode);
    Shared_Ptr<ListNode> next(new ListNode);

    cur->_next = next;
    next->_prev = cur;
}

將_next 與 _prev指標都定義為weak_ptr指標就可以避免出現迴圈引用問題了。

下面來總結一下以上提到的智慧指標:

智慧指標 特點 思想 使用
auto_ptr 帶有缺陷 管理權轉移 嚴禁使用
scoped_ptr 簡單粗暴 防拷貝 鼓勵使用
shared_ptr 相對複雜 引用計數 鼓勵使用,注意迴圈引用問題
weak_ptr