智能指针
首先咱们在理解智能指针以前咱们先了解一下什么是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[].