1. 程式人生 > >c++:分析智慧指標與發展歷史

c++:分析智慧指標與發展歷史

智慧指標是RAII思想的一種產品,那麼什麼是RAII呢?
RAII:Resource Acquistion Is Initialization
資源分配就初始化,定義一個類來封裝資源的分配和釋放。
在建構函式裡完成資源分配初始化,在解構函式裡完成資源的清理。
簡單來舉例:

template <class T>
class AutoPtr
{
public:
    AutoPtr(T* ptr)
        :_ptr(ptr)
    {}

    ~AutoPtr()
    {
        if (_ptr)
        {
            delete _ptr;
        }
    }

private
: T* _ptr; };

這個類就體現了RAII思想。
那麼智慧指標存在的意義是什麼呢?c++用new/delete一樣可以完成資源分配的初始化和清理。
問題就在於執行流的改變:

void Test()
{
    int* p = new int(1);
    if (p)
    {
        return;
    }
    delete p;
}

這段程式碼看似簡單無誤,其實已經存在了記憶體洩漏,因為return改變了執行流,導致無法釋放。
改變執行流的語句還有很多:break,continue,gote,throw等,這些改變執行流的語句都可能導致已經new的空間無法delete,一旦程式碼非常多的時候,我們很難控制。所以使用智慧指標是必要的!

首先,剛才寫的AutoPtr這個類既然叫智慧指標,那麼它就要像個指標,我們有必要實現它的operator*和operator->。

    T& operator*()
    {
        return *_ptr;
    }

    T* operator->()
    {
        return _ptr;
    }

這裡寫圖片描述
(AA就是一個用來舉例的,類有兩個整形的成員變數)
要像個指標還要能賦值,但是當我賦值時: AutoPtr ap3(ap);
這裡寫圖片描述
它崩潰了,原因很好理解,淺拷貝導致的兩個物件的指向同一片空間,那麼析構的時候這片空間必然會析構兩次。還有一個問題,就是一個的改變會影響另一個。這些是我們不希望看到的,所以c++專家們在c++98給出瞭解決方案:auto_ptr
它的核心思想就是管理權轉移,當一個指標把值賦給另一個時,它馬上賦值為NULL。
以下是它的簡單模擬實現:

template <class T>
class AutoPtr
{
public:
    AutoPtr(T* ptr)
        :_ptr(ptr)
    {}

    AutoPtr(AutoPtr<T>& ap)
        :_ptr(ap._ptr)
    {
        ap._ptr = NULL;
    }

    AutoPtr<T>& operator=(AutoPtr<T>& ap)
    {
        if (this != &ap)
        {
            if (_ptr)
            {
                delete _ptr;
            }
            _ptr = ap._ptr;
            ap._ptr = NULL;
        }
        return *this;
    }

    T& operator*()
    {
        return *_ptr;
    }

    T* operator->()
    {
        return _ptr;
    }

    ~AutoPtr()
    {
        if (_ptr)
        {
            delete _ptr;
        }
    }

private:
    T* _ptr;
};

這裡寫圖片描述
這樣做就不會出問題,operator=同理。
但是這樣做有很大的缺陷,ap = NULL;那麼以後就不能使用它,很容易出錯。
現在它已經被禁用。

在c++98提出auto_ptr後,由於有很大缺陷,有人忍的了繼續使用,有人忍不了,所以非官方boost總結了一套方案它們分別是:
scoped_ptr shared_ptr weak_ptr
這裡寫圖片描述
我來分別介紹:
1.scoped_ptr:防拷貝,簡單粗暴

template <class T>
class ScopedPtr
{
public:
    ScopedPtr(T* ptr)
        :_ptr(ptr)
    {}

    T& operator*()
    {
        return *_ptr;
    }

    T* operator->()
    {
        return _ptr;
    }

    ~ScopedPtr()
    {
        if (_ptr)
        {
            delete _ptr;
        }
    }

private:
    //1.定為私有 2.只宣告不定義
    ScopedPtr(ScopedPtr<T>& sp);
    ScopedPtr<T>& operator=(ScopedPtr<T>& sp);

private:
    T* _ptr;
};

直接把拷貝建構函式和operator=定義為私有,而且只宣告,那麼不能賦值就不會出錯,這是個簡單粗暴的辦法!

2.shared_ptr:用了引用計數的方法

template <class T>
class SharedPtr
{
public:
    SharedPtr(T* ptr)
        :_ptr(ptr)
        , _refCount(new int(1))
    {}

    SharedPtr(SharedPtr<T>& sp)
        :_ptr(sp._ptr)
        , _refCount(sp._refCount)
    {
        ++(*_refCount);
    }

    SharedPtr<T>& operator=(SharedPtr<T>& sp)
    {
        if (_ptr != sp._ptr)
        {
            if (--(*_refCount) == 0)
            {
                delete _ptr;
                delete _refCount;
            }
            _ptr = sp._ptr;
            _refCount = sp._refCount;
            ++(*_refCount);
        }
        return *this;
    }

    T& operator*()
    {
        return *_ptr;
    }

    T* operator->()
    {
        return _ptr;
    }

    ~SharedPtr()
    {
        if (--(*_refCount) == 0)
        {
            delete _ptr;
            delete _refCount;
        }
    }

private:
    T* _ptr;
    int* _refCount;
};

可以看出,_refCount用來計數,接下來分析它的賦值:
拷貝構造:
這裡寫圖片描述
operator=:
它有不同的情況:
①自己給自己賦值:sp1 = sp1;或者sp2 = sp1;(sp2已經等於sp1)
②如果被賦值物件的*_refCount == 1,那麼直接釋放它,如果被賦值物件的*_refCount > 1,那就把它的_refCount減1.
這裡寫圖片描述
然後解構函式根據*_refCount來決定是否清理物件,就不會存在淺拷貝的問題。
3.weak_ptr是用來解決shared_ptr的迴圈引用的問題,我在《c++:分析智慧指標shared_ptr存在的迴圈引用的缺陷》一文會詳細分析。

接下來在c++11,官方延用了boost總結的方法,需要注意的是,c++11把scoped_ptr改名為unique_ptr,並且沒有寫陣列。

智慧指標發展歷史的總結:

這裡寫圖片描述
boost和c++11有一些重名的智慧指標,但是不影響使用,因為他們分別在boost::和std::