智能指針

爲什麼要有智能指針?原生態指針的缺陷?
使用原生態指針時我們需要時刻注意空間的申請和釋放,尤其在處理異常時,我們必須在拋出異常前把程序中動態開闢的內存空間釋放掉,有時會使代碼顯得臃腫,所以我們引入智能指針的概念,就是RAII,是一種用對象的生命週期控制程序資源的方式在對象構造期間(構造函數)獲取資源,在對象銷燬時(析構函數)釋放資源,這樣我們就不用顯示的釋放資源,並且對象所需的資源在對象的生命週期內始終有效。
用RAII思想實現簡單的智能指針:

template<class T>
class SmartPtr
{
public:
    SmartPtr(T * p=nullptr)
        :_ptr(p)
    {}
    ~SmartPtr()
    {
        if (_ptr){
            delete _ptr;
        }
    }
private:
    T* _ptr;
};

這段代碼就完成了通過類管理資源的作用,但並不具有指針的特性,即虛加入解引用和->操作方式,如下:

template<class T>
class SmartPtr
{
public:
    SmartPtr(T * p=nullptr)
        :_ptr(p)
    {}
    ~SmartPtr()
    {
        if (_ptr){
            delete _ptr;
        }
    }
    T &operator*()
    {
        return *_ptr;
    }
    T * operator->()
    {
        return _str;
    }
private:
    T* _ptr;
};

用這種方式我們就可以在主函數調用我們自己實現的這個智能指針: SmartPtr<int> p1(new int);就不需要再顯示銷燬它了(已將在析構函數中完成了空間的釋放)。
這種方式雖然可以幫助我們不操心內存的釋放,但執行下面的代碼時:SmartPtr<int> p2(p1);即用p1拷貝構造p2時是淺拷貝,賦值運算同理。


c++98中正式給出了智能指針的概念。那c++98是怎麼解決淺拷貝問題的呢?我們看下面的代碼,

#include<memory>
int main()
{
    auto_ptr<int> p1(new int);
    *p1 = 1;
    auto_ptr<int> p2(p1);
    cout << *p1 << endl;
    cout << *p2 << endl;
    system("pause");
    return 0;
}

它在運行時會崩潰,造成這種崩潰的原因正是這一版本中智能指針處理淺拷貝的方式,它在處理拷貝構造函數和賦值運算符重載時把使用的是資源轉移的方式,把之前的資源轉移給新對象,之前置爲空,所以在解引用訪問p1時就會崩潰,我們可以簡單實現一下這種方式:

template<class T>
class SmartPtr
{
public:
    SmartPtr(T * p = nullptr)
        :_ptr(p)
    {}
    ~SmartPtr()
    {
        if (_ptr){
            delete _ptr;
        }
    }
    SmartPtr(SmartPtr<T> & p)
        :_ptr(p._ptr)
    {
        p._ptr = nullptr;

    }
    SmartPtr<T>& operator=(SmartPtr<T> & p)
    {
        if (this !=& p)
        {
            if (_ptr){//如果_ptr內有資源則先將自己的資源釋放,否則會造成內存泄漏
                delete _ptr;
            }
            _ptr = p._ptr;
            p._ptr = nullptr;
        }
        return *this;
    }
    T &operator*()
    {
        return *_ptr;
    }
    T * operator->()
    {
        return _str;
    }
private:
    T* _ptr;
};

int main()
{
    SmartPtr<int> p1(new int);
    SmartPtr<int> p2(p1);
    SmartPtr<int> p3(new int);
    SmartPtr<int> p2 = p3;//體現賦值運算符第二個if的作用

    system("pause");
    return 0;
}

但上面的方式有個缺陷是將之前的指針與內存斷開聯繫後就不能在對其進行操作,有趣的是在c++03版本中對解決auto_ptr的淺拷貝有了新的方式:新增一bool類型成員變量_owner記錄當前對象是否有權限釋放內存,我們同樣可以簡單實現一下這個版本的智能指針:

template<class T>
class SmartPtr
{
public:
    SmartPtr(T * p = nullptr)
        :_ptr(p)
        , _owner(false)
    {
        if (_ptr){
            _owner = true;
        }
    }

    ~SmartPtr()
    {
        if (_ptr&&_owner){
            delete _ptr;
            _ptr = nullptr;
        }
    }

    SmartPtr(SmartPtr<T> & p)
        :_ptr(p._ptr)
        , _owner(p._owner)//將_owener更新,所以拷貝構造只釋放_owner爲true的
        //解決了淺拷貝的問題
    {
        p._owner = false;
    }
    SmartPtr<T>& operator=(SmartPtr<T> & p)
    {
        if (this != &p){
            //如果當前對象管理了資源先把它釋放
            if (_ptr){
                delete _ptr;
            }
            _ptr = p._ptr;//資源轉移
            _owner = p._owner;//釋放權限轉移
            p._owner = false;
        }
    }
    T &operator*()
    {
        return *_ptr;
    }
    T * operator->()
    {
        return _str;
    }
private:
    T* _ptr;
    bool _owner;
};

用這種方式解決淺拷貝就可以對同時對之前的指針進行操作了,但造成了更大的缺陷,如果使用如下代碼會怎麼樣呢?

int main()
{
    SmartPtr<int> p1(new int);
    if (1){
        SmartPtr<int> p2(p1);
        *p2 = 10;
    }
    //p1是野指針
    *p1 = 20;

    system("pause");
    return 0;
}

由於p1和p2共用同一塊內存空間,在出if作用域後p2將調用其析構函數完成對資源的釋放,所以p1變成野指針,爲我們的代碼造成了隱患,這種危害其實更嚴重,所以在c++11版本中又將智能指針的實現回退到了最開始的RAII模式,並給出了unique_ptr這種並且將拷貝構造函數和賦值運算符重載這兩個默認成員函數禁用,也就不會有淺拷貝的發生了。

#include<memory>

int main()
{
    unique_ptr<int> p1(new int);
    //報錯
    unique_ptr<int> p2(p1);
    unique_ptr<int> p3(new int);
    //報錯
    p3 = p1;
    system("pause"); 
    return 0;
}

這種方式簡單粗暴,我們可以想象一下它的內部實現原理是什麼樣的,在c++98中我們可以將拷貝構造函數和賦值運算符重載這兩個函數給成私有成員函數在類外就無法調用也就避免了淺拷貝的問題。

private:
        unique_ptr(unique_ptr<T> & p){};
        unique_ptr<T> &operator=(unique_ptr<T> &p){};

c++11中將delete關鍵字的作用進行了擴展,作用是跟在默認構造函數後可以禁止調用這個函數:

unique_ptr(const unique_ptr<T> &p) = delete;
        unique_ptr<T>& operator=(const unique_ptr<T>&) = delete;

而在c++98中提供了可以共享資源的智能指針:shared_ptr,它是增加了計數,也就是利用寫時拷貝解決淺拷貝問題的。具體看代碼及註釋:

template<class T>
class DFDef
{
public:
    void operator()(T*&ptr){
        if (ptr){
            delete ptr;
            ptr = nullptr;
        }
    }
};
namespace Mine
{
    template<class T,class DF=DFDef<T>>
    class shared_ptr
    {
    public:
        shared_ptr(T*ptr = nullptr)
            :_ptr(ptr)
            , _pCount(nullptr)
        {
            if (_ptr){
                _pCount = new int(1);//當_ptr不爲空時給_pCount賦值爲1
            }
        }

        ~shared_ptr()
        {
            //當計數>0時只給_pCount--
            //當計爲0時說明當前資源已是最後一個對象在使用,此時由當前對象釋放資源
            if (_ptr && 0 == --(*_pCount)){
                //delete _ptr;
                DF df;
                df(_ptr);
                delete _pCount;
            }
        }

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

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

        //拷貝構造函數和賦值運算符的重載就需要考慮計數的問題了
        shared_ptr(const shared_ptr<T> &sp)
            :_ptr(sp._ptr)
            , _pCount(sp._pCount)
        {
            if (_ptr){
                ++(*_pCount);
            }
        }

        shared_ptr<T> & operator=(const shared_ptr<T> &sp)
        {
            if (this != &sp){
                //1.首先與舊資源斷開聯繫(如果不是最後一個使用資源的對象就只讓計數減一,如果是最後一個使用資源的對象則釋放資源)
                if (_ptr && 0 == -(*_pCount)){
                    delete _ptr;
                    delete _pCount;
                }
                //2.與sp共享資源和計數
                _ptr = sp._ptr;
                _pCount = sp._pCount;
                if (_ptr){
                    ++*_pCount;
                }
            }
            return *this;
        }

        int use_count()
        {
            return *_pCount;
        }
    private:
        T*_ptr;
        int *_pCount;
    };
}

void TestShradPtr()
{
    Mine::shared_ptr<int> sp1(new int);
    cout << sp1.use_count() << endl;

    Mine::shared_ptr<int> sp2(sp1);
    cout << sp1.use_count() << endl;
    cout << sp2.use_count() << endl;

    Mine::shared_ptr<int> sp3(new int);
    cout << sp3.use_count() << endl;

    Mine::shared_ptr<int> sp4(sp3);
    cout << sp3.use_count() << endl;
    cout << sp4.use_count() << endl;

    sp3 = sp2;
    cout << sp2.use_count() << endl;
    cout << sp3.use_count() << endl;

    sp4 = sp2;
    cout << sp2.use_count() << endl;
    cout << sp4.use_count() << endl;
}

int main()
{
    TestShradPtr();
    system("pause");
    return 0;
}

但是shared_pt在循環引用的時候可能會導致資源泄露,比如我們創建一個循環鏈表,其中的_pre和_next都是shared_ptr類型的,那麼將這兩個節點進行首位相連後會發生下面的情況:
*1. node1和node2兩個智能指針對象指向兩個節點,引用計數變成1,我們不需要手動delete。

  1. node1的next指向node2,node2的prev指向node1,引用計數變成2。
  2. node1和node2析構,引用計數減到1,但是next還指向下一個節點。但是prev還指向上一個節點。
  3. 也就是說next析構了,node2就釋放了。
  4. 也就是說prev析構了,node1就釋放了。
  5. 但是next屬於node的成員,node1釋放了,next纔會析構,而node1由prev管理,prev屬於node2
    成員,所以這就叫循環引用,誰也不會釋放。*

所以這兩個節點誰都不會釋放資源,就導致了資源的泄露。
如何解決這個問題,c++又給提供了weak_ptr型的智能指針,只需要將_pre和_next的指針類型改爲weak_ptr即可,原理是weakptr不會增加node1和node2的引用計數。

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