智能指針
首先咱們在理解智能指針以前咱們先了解一下什麼是RAII思想。RAII(Resource Acquisition Is Initialization)機制是Bjarne Stroustrup首先提出的,是一種利用對象生命週期來控制程序資源(如內存、文件句柄、網絡鏈接、互斥量等等)的簡單技術。
對於RAII概念清楚後,咱們就能夠理解爲智能指針就是RAII的一種體現,智能指針呢,它是利用了類的構造和析構,用一個類來管理資源的申請和釋放,這樣作的好處是什麼?咱們來分析一下~node
爲何會有智能指針
咱們來先看一段代碼c++
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最先期的智能指針
咱們先來簡單的用代碼來實現一下:ui
template<class T>
class AutoPtr
{
public:
AutoPtr(T* _ptr) :ptr(_ptr) // 構造
{}this
AutoPtr(const AutoPtr<T>& a) : ptr(a.ptr) //拷貝構造
{
a.ptr = NULL;
// 這裏咱們實現爲NULL ,這個其實也有缺點
// 當咱們要用兩個指針的時候就不行。
}.net
// 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[].