【C++】 淺析智能指針

引言:

由於 C++ 語言沒有自動內存回收機制,程序員每次 new 出來的內存都要手動 delete。程序員忘記 delete,流程太複雜,最終導致沒有 delete,異常導致程序過早退出,沒有執行 delete 的情況並不罕見。



RAII(Resource Acquisition Is Initialization)

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

所謂智能指針就是智能/自動化的管理指針所指向的動態資源的釋放。

STL--auto_ptr

Boost庫的智能指針(ps:新的C++11標準中已經引入了unique_ptr/shared_ptr/weak_ptr)

wKiom1bv4QejFom4AABFmhe2RV0738.png

在這裏,對於aut_optr,scoped_ptr , shared_ptr 進行剖析

一. aut_optr

std::auto_ptr 屬於 STL,當然在 namespace std 中,包含頭文件 #include<memory> 便可以使用。std::auto_ptr 能夠方便的管理單個堆內存對象。爲了解決單個對象被重複釋放多次的情況,我們使用的aut_optr的目的就是轉移權限,顧名思義就是說再拷貝構造新的對象時,由於此時有兩個對象指向同一塊空間,所以將原來的指針賦NULL,然後將這塊空間的所有權交給新的對象指針。

對應代碼:<AutoPtr>

#include<iostream>
using namespace std;
 
template<class T>
class AutoPtr
{
public:
    AutoPtr(T* ptr)
    :_ptr(ptr)
    {}
 
    AutoPtr()
    :_ptr(NULL)
    {}
 
    AutoPtr<T>(AutoPtr<T>& ap)   //權限轉移
        : _ptr(ap._ptr)
    {
        ap._ptr = NULL;
    }
 
    AutoPtr<T>& operator=(AutoPtr<T>& ap)
    {
        if (&ap != this)
        {
            delete _ptr;
            _ptr = ap._ptr;
            ap._ptr = NULL;   //權限轉移
        }
        return *this;
    }
 
    ~AutoPtr()
    {
        if (_ptr)
        {
            delete _ptr;
            _ptr = NULL;
        }
    }
 
    T& operator*()
    {
        return *_ptr;
    }
 
private:
    T* _ptr;
};
 
void Test()
{
    AutoPtr<int> ap1(new int(2));
    AutoPtr<int> ap2 = ap1;
    AutoPtr<int> ap3(new int(3));
    ap3 = ap1;
}
 
int main()
{
    Test();
    return 0;
}

wKiom1bv52GRW4a5AAAK7gT15DQ367.png雖然表面上解決了重複釋放的問題,但是卻存在一個很大問題,請看右圖:

(先忽略 s 的存在)

若有s2對象,指向自己的一塊空間,現在要拷貝構造一個s3對象,那麼順應上面的代碼,s2會被置NULL,而這塊空間重歸s3所管理。

如果考慮到這塊空間已經有多個指針指向,即s和s2,那麼再拷貝構造s3時,s2被置空,此時,s變成垂懸指針。這就是一個打的問題所在!


二. scoped_ptr

實用的智能指針,思想就是防拷貝,在大多時候用不到拷貝構造和賦值運算符重載,那麼我們做的就是寫出構造函數和析構函數,拷貝構造和賦值運算符重載只聲明不定義。這裏有幾點要說明:


<1>在代碼中,將拷貝構造和賦值運算符重載設置成保護或者私有的,爲什麼?世界之大,總有些人想害朕。。話歸主題,原因是防止有壞人在外面修改,倘若被修改也不會調用修改的,因爲無法訪問得到。

<2>爲什麼要聲明拷貝構造和賦值函數,原因是如果不聲明,編譯器會調用自己默認的構造和賦值函數,那麼防拷貝就無從談起。

代碼如下:<ScopedPtr>

#include<iostream>
using namespace std;
 
template<class T>
class ScopedPtr
{
public:
    ScopedPtr(T* ptr)
        :_ptr(ptr)
    {}
 
    Scoped()
        :_ptr(NULL)
    {}
 
    ~ScopedPtr()
    {
        if (_ptr)
        {
            delete _ptr;
            _ptr = NULL;
        }
    }
 
    T& operator*()
    {
        return *_ptr;
    }
    T* operator->()
    {
        return _ptr;
    }
 
    T* GetPtr()
    {
        return _ptr;
    }
 
protected:
    ScopedPtr<T>(const ScopedPtr<T>& sp);// 只聲明不定義,防拷貝
    ScopedPtr<T>& operator = (const ScopedPtr<T>& sp);
 
private:
    T* _ptr;
};
 
void Test()
{
    ScopedPtr<int> sp1(new int(2));
    ScopedPtr<int> sp2 = sp1;
    ScopedPtr<int> sp3(new int(3));
    sp3 = sp1;
}
 
int main()
{
    Test();
    return 0;
}

auto_ptr和scopedptr的取捨:

boost::scoped_ptr 用於確保動態分配的對象能夠被正確地刪除。scoped_ptr 有着與std::auto_ptr類似的特性,而最大的區別在於它不能轉讓所有權而auto_ptr可以。事實上,scoped_ptr永遠不能被複制或被賦值!scoped_ptr 擁有它所指向的資源的所有權,並永遠不會放棄這個所有權。scoped_ptr的這種特性提升了我們的代碼的表現,我們可以根據需要選擇最合適的智能指針(scoped_ptr 或 auto_ptr)。要決定使用std::auto_ptr還是boost::scoped_ptr, 就要考慮轉移所有權是不是你想要的智能指針的一個特性。如果不是,就用scoped_ptr. 它是一種輕量級的智能指針;使用它不會使你的程序變大或變慢。它只會讓你的代碼更安全,更好維護。


三. sharede_ptr

在上面我們看到 boost::scoped_ptr 獨享所有權,不允許賦值、拷貝,boost::shared_ptr 是專門用於共享所有權的,由於要共享所有權,其在內部使用了引用計數。boost::shared_ptr 也是用於管理單個堆內存對象的。wKioL1bv8Oiw3BzTAAAOMwd2MNI898.png


這個智能指針解決了auto_ptr獨佔的問題,採用引用計數的方法,一旦最後一個這樣的指針被銷燬,也就是一旦某個對象的引用計數變爲0,這個對象會被自動刪除。這在非環形數據結構中防止資源泄露很有幫助。


用法:刪除共用對象


對應代碼:<SharedPtr>

#include<iostream>
using namespace std;
 
template<class T>
class SharedPtr
{
public:
    SharedPtr(T* ptr)
        :_ptr(ptr)
        , _pCount(new long(1))
    {}
 
    SharedPtr()
        :_ptr(NULL)
        , _pCount(new long(1))
    {}
 
    SharedPtr<T>(const SharedPtr<T>& sp)
        : _ptr(sp._ptr)
        , _pCount(sp._pCount)
    {
        ++(*_pCount);
    }
 
    SharedPtr<T>& operator=(const SharedPtr<T>& sp)
    {
        if (&sp != this)
        {
            if (--(*_pCount) == 0) // 減到0釋放對象一次
            {
                delete _ptr;
                delete _pCount;
            }
            _ptr = sp._ptr;
            _pCount = sp._pCount;
            ++(*_pCount);
        }
        return *this;
    }
 
    ~SharedPtr()
    {
        if (_ptr)
        {
            if (--(*_pCount) == 0)
            {
                delete _ptr;
                delete _pCount;
            }
        }
    }
 
    T& operator*()
    {
        return *_ptr;
    }
    
    T* operator->()
    {
        return _ptr;
    }
 
    long GetCount()
    {
        return *(_pCount);
    }
 
    T* GetPtr()
    {
        return _ptr;
    }
 
private:
    T* _ptr;
    long* _pCount;
};
 
 
void Test()
{
    SharedPtr<int> sp1 = new int(1);
    SharedPtr<int> sp2 = sp1;
    SharedPtr<int> sp3 = new int(2);
    sp3 = sp1;
}
 
 
int main()
{
    Test();

    return 0;
}

但是shared_ptr看起來完美,但是也存在一下問題:

  1.  引用計數更新存在着線程安全

  2.循環引用

  3.定置刪除器


我們先在這裏討論第二種情況,即循環引用問題

我們引出循環引用的場景圖:

wKiom1bzrMzS_i8LAAAIJIxbnM4972.png

此時,兩個節點的引用計數都爲2,因爲有兩個指針分別指向a和b,此時就有了一個問題,_next析構時等着_prev析構,_prev要析構,只能b析構;而_prev也在等_next析構,_next要析構,只能a析構。這樣就有個問題類似”你等我,我等你,無休止,永遠不會析構,一直循環“。

要解決這個問題,就又要引出弱指針:weak_ptr


weak_ptr唯一的功能就是解決shared_ptr的循環引用問題,那是怎麼解決的呢?

我們看代碼:

#include <boost/shared_ptr.hpp>
#include <boost/weak_ptr.hpp>
using namespace boost;

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

     //weak_ptr<ListNode > _prev; // 在此,weak_ptr可以解決循環引用的問題
     //weak_ptr<ListNode > _next;

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

void Test ()
{
     // 循環引用問題
     shared_ptr <ListNode > p1( new ListNode ());
     shared_ptr <ListNode > p2( new ListNode ());

     cout <<"p1->Count:" << p1. use_count()<<endl ;// use_count是庫裏的引用計數
     cout <<"p2->Count:" << p2. use_count()<<endl ;

     // p1節點的_next指向 p2節點
     p1->_next = p2;
     // p2節點的_prev指向 p1節點 
     p2->_prev = p1;

     cout <<"p1->Count:" << p1. use_count ()<<endl ;
     cout <<"p2->Count:" << p2. use_count ()<<endl ;
}

這樣問題就得到了解決。

至此,我們也知道了,當需要寫一個智能指針時,我們儘可能的去寫scoped_ptr或者shared_ptr,而千萬不要寫auto_ptr,問題上面已經詳細分解。

偏文不到之處,還請評正。

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