1. 程式人生 > >C++中的三種智慧指標分析(RAII思想)

C++中的三種智慧指標分析(RAII思想)

智慧指標
首先我們在理解智慧指標之前我們先了解一下什麼是RAII思想。RAII(Resource Acquisition Is Initialization)機制是Bjarne Stroustrup首先提出的,是一種利用物件生命週期來控制程式資源(如記憶體、檔案控制代碼、網路連線、互斥量等等)的簡單技術。 
對於RAII概念清楚後,我們就可以理解為智慧指標就是RAII的一種體現,智慧指標呢,它是利用了類的構造和析構,用一個類來管理資源的申請和釋放,這樣做的好處是什麼?我們來分析一下~

為什麼會有智慧指標
我們來先看一段程式碼

void Fun()
{
    int *p = new int[1000];
    throw int(); //異常的丟擲
    delete[] p;
}

int main()
{
    try
    {
        Fun();
    }
    catch (exception e)
    {
        printf("異常\n"); // 捕捉
    }
    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
上面程式碼我們可以看看,在我們寫的程式碼中,如果在執行中我們開出了1000個int位元組的空間,但是在我們遇見throw後,因為異常而導致Fun函式沒有執行到delete[],所以就會造成記憶體洩漏問題,這樣的話對於一個伺服器程式來說是不能允許的。 
這個只是一個簡單的例子,其實還有很多,比如我們在寫程式碼的時候往往會開啟一個檔案來進行讀寫操作,然後又是因為異常,導致我們開啟的檔案沒有及時的關閉,從而造成檔案描述符洩漏也可以說是記憶體洩漏。

為了防止這種場景的發生,我們採用了智慧指標。

智慧指標分類
在C++的歷史長河中,不斷髮展不斷進步,所以在智慧指標上也做了很大優化和該進,今天我們就看看有那些指標類。

auto_ptr類
scoped_ptr類
share_ptr類+weak_ptr類
1
2
3
有三種智慧指標的類。現在我們最好可靠的是share_ptr。 
我們先在分別介紹一下各自的特點以及簡單的實現。

auto_ptr最早期的智慧指標
我們先來簡單的用程式碼來實現一下:

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

    AutoPtr(const AutoPtr<T>& a) : ptr(a.ptr) //拷貝構造
    {
        a.ptr = NULL; 
        // 這裡我們實現為NULL ,這個其實也有缺點
        // 當我們要用兩個指標的時候就不行。
    }

    // NULL指標也可以釋放
    AutoPtr<T>& operator=(AutoPtr<T>& a) // 賦值運算子過載
    {
        if (this != &a)
        {
            delete ptr;
            // 賦值過程中,如果涉及原有空間,一定要先釋放。
            // 還有在引用計數或者寫實拷貝要先判斷上一個
            // 是否被解構函式要減減它的引用計數
            ptr = a.ptr;
            a.ptr = NULL;
        }
        return *this;
    }

    ~AutoPtr() //析構
    {
        delete ptr;
    }

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

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

private:
    T* ptr;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
我們實現完aotu_ptr最先感覺就是對指標的一層封裝,是的!但是有所不同的是我們交給它管理空間,就不會有最開始說的問題!

在C++早期的智慧指標,並不好,為什麼這麼說呢?因為如果我們要對aotu_ptr進行賦值或者拷貝構造,那麼被拷貝的將會為空,這樣就為我們的寫程式碼加大難度,同時出錯率也大大的提高。很容易造成訪問越界。

所以後來人們就出現第二種~

scoped_ptr防拷貝賦值智慧指標
出現這種智慧指標,和上面很相似,但是這個處理上面問題的方式很是暴力,直接把賦值與拷貝寫成私有宣告。就跟本不能用。這個一定程度上減少程式碼的出錯率,但是同時也產生了一定的侷限性。 
我們來看程式碼

template<class T> //模板實現
class Scoped_ptr
{
public:
    Scoped_ptr(T* _ptr) :ptr(_ptr) // 構造
    {}

    ~Scoped_ptr() //析構
    {
        if (ptr != NULL)
        {
            delete ptr;
        }
    }

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

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

private:
    Scoped_ptr(const Scoped_ptr<T>& s); // 私有防止拷貝
    Scoped_ptr<T>& operator=(const Scoped_ptr<T>& s); // 防止賦值
    T* ptr;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
更為簡單,但是這不能滿足人們的工作場景,所以就有了更為穩妥的一種方式

share_ptr採用引用計數的方式
我們先來看程式碼 
這個程式碼我們只是簡單模擬實現,不是stl庫中實現方式

template<class T> //模板
class Share_ptr
{
    // weak_ptr的friend 
    friend class Weak_ptr<T>; //這裡先不用在意,稍後解釋
public:
    Share_ptr(T* _ptr) :ptr(_ptr), pCount(new int(1)) //構造
    {}

    Share_ptr(const Share_ptr<T>& s)  // 拷貝構造
    :ptr(s.ptr), pCount(s.pCount) // 這是一個指標
    {
        ++(*pCount); //對引用計數進行++
    }

    ~Share_ptr() //析構
    {
        if (*pCount == 1)
        {
            delete ptr;
            delete pCount;
        }
        else
        {
            --(*pCount);
        }
    }

    Share_ptr<T>& operator=(const Share_ptr<T>& s) //賦值過載
    {
        if (ptr != s.ptr)
        {
            if (--(*pCount) == 0)
            {
                if (ptr)
                {
                    delete ptr;
                }
                delete pCount;
            }
            ptr = s.ptr;
            pCount = s.pCount;
            ++(*pCount); // 注意 
        }
        return *this;
    }

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

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

private:
    T* ptr;
    int* pCount; //這個採用引用計數時的計數器
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
share_ptr採用了引用計數的方式,更好解決了賦值與拷貝的問題。 
引用計數:我們來講解一下,就是在構造出一個物件指標後,我們也了一個*count這樣的計數器,值就是1,當我們需要拷貝或者賦值的時候,我們就將 *count加1,讓物件的指標指向同一塊空間,每個指標都能狗通過指標物件來訪問指向的空間,我們其中某一個物件要是宣告週期完了,自動掉用解構函式,這時候,我們的解構函式中就會判斷引用計數是否為1,如果不是1說明這段空間還有別的物件在用,那麼將會對 *count的計數器中的值減1,當 *count值為1的時候,並且改物件要析構,這時候才會正真的釋放這段空間。

雖說share_ptr比前面的兩個指標指標都要好,但是在一種場景下share_ptr是不行的,什麼呢?

我們假如用share_ptr管理一個雙向連結串列的結構,這個時候就會出現記憶體洩漏,為什麼呢?因為在管理連結串列中,當一個節點的next指向下一個,下一個指向上一個的時候,我們的引用計數也在增加,這個時候,就會出現迴圈引用,具體情況是什麼樣子呢?我們用圖來解釋~ 
 
 
所以為了解決這個問題就有個一個輔助的指標類,也叫弱指標。

weak_ptr輔助share_ptr智慧指標
我們看模擬實現的程式碼~

template<class T>
class Weak_ptr
{
public:
    Weak_ptr(const Share_ptr<T>& s) :ptr(s.ptr) //構造
    {}

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

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

private:
    T* ptr;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
其實就是把原生指標進行了一層封裝。所以不用自己實現析構,用預設就可以。 
還有一點就是因為weak_prt是share_ptr的輔助,而weak_ptr中需要訪問share_ptr的私有成員所以我們要在share_ptr中宣告成友元類。

關於share_ptr與weak_ptr怎麼解決像上面一樣的場景,我們來看一下:


// 迴圈引用,會造成記憶體洩漏weak_ptr
struct ListNode //連結串列結構體
{
    Weak_ptr<ListNode> next; //這裡為weak_ptr<ListNode>型別
    Weak_ptr<ListNode> prev;
    int data;

    ListNode() :next(NULL), prev(NULL) //構造
    {}
};

// 在用share_ptr的時候要注意,share_ptr<ListNode> next; 
// 這裡的share_ptr<ListNode>本身就是一個指標。

void TestListNode()
{
    Share_ptr<ListNode> node1 = new ListNode; //為share_ptr
    Share_ptr<ListNode> node2 = new ListNode;

    // 這裡要解釋一下,node1為share_ptr型別過載->,
    // 而next是weak_ptr型別,後面node2是一個share_ptr型別
    // 這裡就有一個隱式型別轉換
    node1->next = node2;
    node2->next = node1;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
在說完share_ptr智慧指標,不知到有沒有發現,我們申請空間都是一個一個型別的大小,釋放也是一個而不是 []。就比如:我們要用share_ptr管理一個10個int的大小。 
那麼我們實現的將不能自己正確的釋放空間。所以我們要介紹一個仿函式

仿函式
關於仿函式如果有了解STL中的六大組建,就會知道其中有一個就叫做仿函式,仿函式具體是怎麼實現的呢?

其實很簡單就是,在另一個類中過載一下(),這樣我們就可以通過物件的()來對進行傳引數,就像是函式呼叫一樣,我們來用程式碼來看看

template<class T>
struct DeleteArray
{
    void operator()(T* ptr) // 用來釋放指標所指向的空間
    {
        delete[] ptr; 
    }
};
1
2
3
4
5
6
7
8
9
這個類中就過載了(),沒有做其他事情,那麼我們就在用的時候直接用它的匿名物件進行()呼叫,就可以實現仿函式。如果這個不是很清楚,那麼我們在看一個例子:

// 用了仿函式
struct Less
{
    // 對()的過載
    int operator()(int x, int y)
    {
        return x+y;
    }
    // 用物件呼叫過載的()
};
int main()
{
    Less a;
    std::cout << a(1,8) << std::endl; // 就像函式一樣呼叫
    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
用這個有什麼用處呢? 
第一就是我們前面所提到的用new[] 開闢出來的空間我們必須要用delete[]來進行釋放,所以我們要在share_ptr中傳入仿函式,用來適應不同的場景。 
仿函式也有很多用處,比如,我們在STL中,用演算法排序的時候,演算法肯定要知道從大到小函式從小到大,所以我們傳一個仿函式,就可以解決,增加了靈活性。 
因為在c++庫中,share_ptr實現非常複雜,同時就實現在用法上稍微簡單了一點,比如:

#include <memory>
 share_ptr<string> p(new string[10], 物件);   物件過載了()
 // 注意:這裡的物件用來給share_ptr做定製刪除器
1
2
3
後面的物件就是要傳入的仿函式。因為我們前面建立了[],所以仿函式就是要有delete[].

如果有誤, 還望多多指導!謝謝!
--------------------- 
作者:GangStudyIT 
來源:CSDN 
原文:https://blog.csdn.net/GangStudyIT/article/details/80645399 
版權宣告:本文為博主原創文章,轉載請