C++程序设计中,使用堆内存是很繁琐的操作——堆内存的申请和释放都需要程序员自己去管理。虽然说程序员自己管理内存可以提高程序的效率,但是整体来说程序员手动管理内存是比较麻烦的,而且容易出现内存泄漏,异常安全(如果在malloc和free之间如果存在抛异常,那么还是有内存泄漏)等问题。而在C++11中引入了智能指针的概念
来管理堆内存。
智能指针的实现采用了一种RAII(利用对象生命周期来控制程序资源)的技术对普通的指针进行封装,使得智能指针实际是一个对象,其行为表现的却像一个指针。也就是说,在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。那么我们就不需要显式地去释放资源,且对象所需要的资源在生命周期内始终有效。
auto_ptr
简单模拟实现:
1 | template<class T> |
由以上代码可以看到,auto_ptr的实现是利用了管理权转移的思想——一旦发生拷贝,就将ap/sp中资源转移到当前对象中,然后令ap/sp与其所管理资源断开联系,这样就解决了一块空间被多个对象使用而造成程序奔溃问题。
但是, 通过实现原理层来分析会发现,这里拷贝后把ap对象的指针赋空了,导致ap对象悬空,通过ap对象访问资源时就会出现问题,于是,引出了一种资源管理权转移的方式
1 | template<class T> |
unique_ptr
因为在拷贝时经常会出现一些问题,所以引出了一种简单粗暴的方式——禁止拷贝,就问你怕不怕!!!
也就是说,将类中的拷贝构造函数和运算符重载函数设为私有的,使得资源独占,只能被一个对象使用
1 | 模拟实现: |
shared_str:
在C++11中,引入了shared_str这一更靠谱且支持拷贝的智能指针。
shared_str的原理是:
- 通过引用计数的方式来实现多个shared_str对象之间共享资源。也就是说:shared_str在其内部,为每个资源都维护了一份计数,用来记录该资源被几个对象共享。
- 在对象被销毁时,就说明该对象不使用这一资源了,对象的引用计数减一。
- 当引用计数减为0时,说明自己是最后一个使用该资源的对象必须释放该资源。
如果不是0,说明该资源还被其他的对象使用着,就不能释放该资源,否则其他对象就会成为野指针
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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127//采用引用计数的方式
//为了线程安全,需要上锁,但是可能会造成死锁(锁内调用的其他代码如果抛异常则会造成死锁),守卫锁,进入时创建对象,函数退出时自动销毁
// 模拟实现一份简答的SharedPtr,了解原理
#include <thread>
#include <mutex>
template <class T>
class SharedPtr
{
public:
SharedPtr(T* ptr = nullptr)
: _ptr(ptr)
, _pRefCount(new int(1))
, _pMutex(new mutex)
{
// 如果是一个空指针对象,则引用计数给0
if (_ptr == nullptr)
*_pRefCount = 0;
}
~SharedPtr()
{
Release();
}
SharedPtr(const SharedPtr<T>& sp)
: _ptr(sp._ptr)
, _pRefCount(sp._pRefCount)
, _pMutex(sp._pMutex)
{
// 如果是一个空指针对象,则不加引用计数,否则才加引用计数
if (_ptr)
AddRefCount();
}
// sp1 = sp2
SharedPtr<T>& operator=(const SharedPtr<T>& sp)
{
//if (this != &sp)
if (_ptr != sp._ptr)
{
// 释放管理的旧资源
Release();
// 共享管理新对象的资源,并增加引用计数
_ptr = sp._ptr;
_pRefCount = sp._pRefCount;
_pMutex = sp._pMutex;
if (_ptr)
AddRefCount();
}
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
int UseCount()
{
return *_pRefCount;
}
T* Get()
{
return _ptr;
}
int AddRefCount()
{
// 加锁或者使用加1的原子操作
_pMutex->lock();
++(*_pRefCount);
_pMutex->unlock();
return *_pRefCount;
}
int SubRefCount()
{
// 加锁或者使用减1的原子操作
_pMutex->lock();
--(*_pRefCount);
_pMutex->unlock();
return *_pRefCount;
}
private:
void Release()
{
// 引用计数减1,如果减到0,则释放资源
if (_ptr && SubRefCount() == 0)
{
delete _ptr;
delete _pRefCount;
}
}
private:
int* _pRefCount; // 引用计数
T* _ptr; // 指向管理资源的指针
mutex* _pMutex; // 互斥锁
};
int main()
{
SharedPtr<int> sp1(new int(10));
SharedPtr<int> sp2(sp1);
*sp2 = 20;
cout << sp1.UseCount() << endl;
cout << sp2.UseCount() << endl;
SharedPtr<int> sp3(new int(10));
sp2 = sp3;
cout << sp1.UseCount() << endl;
cout << sp2.UseCount() << endl;
cout << sp3.UseCount() << endl;
sp1 = sp3;
cout << sp1.UseCount() << endl;
cout << sp2.UseCount() << endl;
cout << sp3.UseCount() << endl;
return 0;
}
std::shared_str的线程安全问题:
通过以上的代码细心的同学就会发现了:在对引用计数进行加1,减1操作时我们对其进行了上锁操作。
那么为什么要对其进行上锁操作呢?细想一下其实也很简单:
1.智能指针对象中引用计数是多个智能指针对象共享的,两个线程中智能指针的引用计数同时++或–,这个操作不是原子的,引用计数原来是1,++了两次,可能还是2。这样引用计数就错乱了。会导致资源未释放或者程序崩溃的问题。所以只能指针中引用计数++、–是需要加锁的,也就是说引用计数的操作必须是线程安全的。
2.智能指针管理的对象存放在堆上,两个线程中同时去访问,会导致线程安全问题
std::shared_str的循环引用问题:
1 | struct ListNode |
在上边代码上可以看出一个问题——循环引用,分析如下:
- node1和node2两个智能指针对象指向两个节点,引用计数变成1,我们不需要手动delete。
- node1的_next指向node2,node2的_prev指向node1,引用计数变成2。
- node1和node2析构,引用计数减到1,但是_next还指向下一个节点。但是_prev还指向上一个节点。
- 也就是说_next析构了,node2就释放了。
- 也就是说_prev析构了,node1就释放了。
- 但是_next属于node的成员,node1释放了,_next才会析构,而node1由_prev管理,_prev属于node2
成员,所以这就叫循环引用,谁也不会释放。
1 | // 解决方案:在引用计数的场景下,把节点中的_prev和_next改成weak_ptr就可以了 |