天天看點

智能指針分析

為什麼有智能指針?

在我們寫代碼時,經常會忘記釋放掉動态開辟出來的記憶體,或者在我們的程式中,new和delete中間有如 throw/goto/return/break 這樣引起執行流改變的語句時,就會造成沒有釋放資源,造成記憶體洩漏。

void Test1()
{
int* p=new int[];
Test();
...
delete[] p;
           

調用的Test2()中如果有抛異常的話,就會無法釋放p,造成記憶體洩漏,

這種情況就需要用智能指針來解決。

智能指針

所謂“智能指針”就是比一般的指針聰明,它可以智能/自動的釋放掉指針所指向的動态資源空間。

那麼這種智能指針是任何實作的呢?

在這裡我們要提到一種思想

RAII(Resource Acquisition Is Initialization)

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

是以智能指針其實就是一個類

智能指針(自動釋放,可以像指針一樣用)

1、自動釋放

一個類裡(構造,析構)

//RAII思想

2、像指針一樣

通過運算符的重載實作(* 和->)

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

T* operator-> ()
{
return _ptr;
}
注意函數傳回值類型
           

上面說了智能指針是什麼,以及為什麼存在這種智能指針,那麼下面我們再來看看智能指針都有那些代表,以及它的發展

早期c++98 auto_ptr 管理權轉移,帶有缺陷
boost scoped_ptr 守衛指針 防拷貝
scoped_arrary
shared_ptr 共享指針 引用計數,缺陷循環引用
shared_array
weak_ptr 弱指針 不單獨存在,輔助解決shared_ptr的循環引用問題

一、模拟實作AutoPtr指針

// 智能指針--類(AutoPtr指針)

//首先是個類
template<class T>
class AutoPtr
{
public:
    AutoPtr(T* ptr)//構造
        :_ptr(ptr)
    {}

    ~AutoPtr()//析構,清理資源
    {
        if(_ptr)
        { 
            printf("0x%p\n", _ptr);
            delete _ptr;
        }
    }

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

    // ap1 = ap2
    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;
    }

private:
    T* _ptr;
};

struct AA
{
    int _a1;
    int _a2;
};

//寫個函數對比測試一下
void Func ()
{
    int* p1 = new int();
    *p1 = ;

    // RAII
    AutoPtr<int> ap1(new int());
    *ap1 = ; //ap1.operator*() = 20;


    AA* p2 = new AA;
    p2->_a1 = ;
    p2->_a2 = ;

    AutoPtr<AA> ap2(new AA);
    ap2->_a1 = ;
    ap2->_a2 = ;
}

void TestAutoPtr()
{
    //Func();
    int* p1 = new int();
    int* p2 = p1;//沒有釋放記憶體,清理對象

    // 深拷貝
    AutoPtr<int> ap1(new int());
    AutoPtr<int> ap2 = ap1;

    AutoPtr<int> ap3(new int());
    ap2 = ap3;
}
           

分析:

與普通指針的異同

智能指針分析
智能指針分析

對比普通指針和智能指針,智能指針可以實作*/->兩個功能,同時可以自動清理資源

管理權的轉移

智能指針分析
智能指針分析

我們可以看到這裡就展現出了auto_ptr這種智能指針的特點,也是它的缺陷,當另一個指針指向時,之前的指針就對它沒有任何權利了,變為空指針,權利全部給新的指針,一個auto_ptr,隻能管理一塊空間(就像一個人隻能有一個男朋友/女朋友,分手後,前女友就和你沒有關系了,珍惜現女友)

二、模拟實作Scoped_ptr指針

這個指針的特點是防拷貝

如何實作防拷貝

1.隻聲明不實作

2.私有

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

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

    // 像指針一樣
    T& operator*()
    {
        return *_ptr;
    }

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

    // 防拷貝
    // 1.隻聲明不實作
    // 2.私有
private:
    ScopedPtr(const ScopedPtr<T>& sp);
    ScopedPtr<T>& operator=(const Scopedtr<T>& sp);
    //将拷貝構造和operator=封在類裡,設為私有

private:
    T* _ptr;
};

void TestScopedPtr()
{
    ScopedPtr<int> sp1(new int());
    //ScopedPtr<int> sp2(sp1);錯誤使用
    ScopedPtr<int> sp3(new int());
    //sp1 = sp3;錯誤使用
}
           

三、Shared_ptr共享指針&Weak_ptr弱指針

(這兩個指針要結合起來使用)

auto_ptr能共享空間,scoped_ptr不能拷貝,都很bug,但我們還有shared_ptr(當然它也有循環引用的問題,是以有弱指針輔助)

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

    ~SharedPtr()
    {
        if (--(*_refCount) == )
        {
            cout <<"釋放了 "<< endl;
            delete _ptr;
            delete _refCount;
        }
    }

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

    // sp2 = sp1
    SharedPtr<T>& operator=(const SharedPtr<T>& sp)
    {
        if (_ptr != sp._ptr)
        {
            if (--(*_refCount) == )
            {
                delete _ptr;
                delete _refCount;
            }

            _ptr = sp._ptr;
            _refCount = sp._refCount;
            (*_refCount)++;
        }

        return *this;
    }

    T& operator*()
    {...}

    T* operator->()
    {...}

    int UseCount()
    {
        return *_refCount;
    }

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

上面的程式隻是shared_ptr的部分功能的思想展現,不是真正的模拟shared_ptr指針,真正的shared_ptr在boost庫裡是很複雜的。

下面我們來說剛才寫道的循環引用問題

struct ListNode
{
    int _data;
    SharedPtr<ListNode> _next;
    SharedPtr<ListNode> _prev;
    /*WeakPtr<ListNode> _next;
    WeakPtr<ListNode> _prev;*/

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

void TestSharedPtrRef()
{
    SharedPtr<ListNode> cur(new ListNode);
    SharedPtr<ListNode> next(new ListNode);

    cout<<cur.UseCount()<<endl;//1
    cout<<next.UseCount()<<endl;//1

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

    cout<<cur.UseCount()<<endl;//2
    cout<<next.UseCount()<<endl;//2
}
           
智能指針分析

這裡的循環引用問題出現在了哪呢?

當我們調析構函數釋放空間時,就出問題了

當我們要是放cur時,如果把cur直接釋放,那麼next的_prev就成了野指針。同理next節點也是一樣的。 (其實就是當要釋放cur節點時,先要清理掉它内部的指針,可是cur->next指向next,next->prev又指向cur,是以他們就成了一個死循環,一直就無法釋放)

是以循環引用就是兩個對象互相牽制了對方,進而導緻兩個對象誰都無法被釋放,引發了記憶體洩露

智能指針分析

運作程式時就會發現程式死循環了

是以為了解決這種問題,我們就要用weak_ptr我們隻需要将節點的_next和_prev計數就好,不需要把shared_ptr的指針也計數,就可以解決問題

template<class T>
class WeakPtr
{
public:
    WeakPtr()
        :_ptr(NULL)
    {}

    WeakPtr(const SharedPtr<T>& sp)
        :_ptr(sp._ptr)
    {}

    WeakPtr<T>& operator=(const SharedPtr<T>& sp)
    {
        _ptr = sp._ptr;
        return *this;
    }

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

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

private:
    T* _ptr;
};
           

繼續閱讀