智能指针
# 内存泄漏
内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不 是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而 造成了内存的浪费。
内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会 导致响应越来越慢,最终卡死。
# 避免内存泄漏
- 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。
- 采用RAII思想或者智能指针来管理资源。
- 有些公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。
总结: 内存泄漏非常常见,解决方案分为两种:1、事前预防型。如智能指针等。2、事后查错型。如泄漏检测工 具。
# RAII
RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句 柄、网络连接、互斥量等等)的简单技术。 在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的 时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:
- 不需要显式地释放资源。
- 采用这种方式,对象所需的资源在其生命期内始终保持有效。
int div() {
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void func() {
int* ptr = new int;
try {
cout << div() << endl;
}
catch (...) {
delete ptr;
throw;
}
delete ptr;
}
int main() {
try {
func();
}
catch (exception& e) {
cout << e.what() << endl;
}
return 0;
}
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
但是这种抛异常的解决方案并不完美,因此引出了智能指针这一方式
template<class T>
class SmartPtr {
public:
SmartPtr(T* ptr) :_ptr(ptr) {}
~SmartPtr() {
cout << "delete: " << _ptr << endl;
delete _ptr;
}
T& operator*() {
return *_ptr;
}
T* operator->() {
return _ptr;
}
private:
T* _ptr;
};
int div() {
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void func() {
SmartPtr<int> sp(new int);
//...
cout << div() << endl;
//...
}
int main() {
try {
func();
}
catch (exception& e) {
cout << e.what() << endl;
}
return 0;
}
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
代码中将申请到的内存空间交给了一个SmartPtr对象进行管理。
在构造SmartPtr对象时,SmartPtr将传入的需要被管理的内存空间保存起来。 在SmartPtr对象析构时,SmartPtr的析构函数中会自动将管理的内存空间进行释放。
这样一来,无论程序是正常执行完毕返回了,还是因为某些原因中途返回了,或是因为抛异常返回了,只要SmartPtr对象的生命周期结束就会调用其对应的析构函数,进而完成内存资源的释放。
智能指针的原理就是:
- 在对象构造时获取资源,在对象析构的时候释放资源,利用对象的生命周期来控制程序资源,即RAII特性。
- 对
*
和->
运算符进行重载,使得该对象具有像指针一样的行为。
# auto_ptr
管理权转移
包含在memory
这个头文件中,C++98中引入的智能指针,auto_ptr通过管理权转移的方式解决智能指针的拷贝问题,保证一个资源在任何时刻都只有一个对象在对其进行管理,这时同一个资源就不会被多次释放了。
int main() {
std::auto_ptr<int> ap1(new int(1));
std::auto_ptr<int> ap2(ap1);
*ap2 = 10;
//*ap1 = 20; //error
std::auto_ptr<int> ap3(new int(1));
std::auto_ptr<int> ap4(new int(2));
ap3 = ap4;
return 0;
}
2
3
4
5
6
7
8
9
10
11
但一个对象的管理权转移后也就意味着,该对象不能再用对原来管理的资源进行访问了,否则程序就会崩溃,因此使用auto_ptr之前必须先了解它的机制,否则程序很容易出问题。
# unique_ptr
防拷贝
unique_ptr是C++11中引入的智能指针,unique_ptr通过防拷贝的方式解决智能指针的拷贝问题,也就是简单粗暴的防止对智能指针对象进行拷贝,这样也能保证资源不会被多次释放。
int main() {
std::unique_ptr<int> up1(new int(0));
//std::unique_ptr<int> up2(up1); //error
return 0;
}
2
3
4
5
但防拷贝其实也不是一个很好的办法,因为总有一些场景需要进行拷贝。
# shared_ptr
引用计数
shared_ptr是C++11中引入的智能指针,shared_ptr通过引用计数的方式解决智能指针的拷贝问题。
- 每一个被管理的资源都有一个对应的引用计数,通过这个引用计数记录着当前有多少个对象在管理着这块资源。
- 当新增一个对象管理这块资源时则将该资源对应的引用计数进行++,当一个对象不再管理这块资源或该对象被析构时则将该资源对应的引用计数进行--。
- 当一个资源的引用计数减为0时说明已经没有对象在管理这块资源了,这时就可以将该资源进行释放了。
通过这种引用计数的方式就能支持多个对象一起管理某一个资源,也就是支持了智能指针的拷贝,并且只有当一个资源对应的引用计数减为0时才会释放资源,因此保证了同一个资源不会被释放多次。
int main() {
cl::shared_ptr<int> sp1(new int(1));
cl::shared_ptr<int> sp2(sp1);
*sp1 = 10;
*sp2 = 20;
cout << sp1.use_count() << endl; //2
// use_count成员函数,用于获取当前对象管理的资源对应的引用计数
cl::shared_ptr<int> sp3(new int(1));
cl::shared_ptr<int> sp4(new int(2));
sp3 = sp4;
cout << sp3.use_count() << endl; //2
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
# weak_ptr
解决循环引用问题
weak_ptr是C++11中引入的智能指针,weak_ptr不是用来管理资源的释放的,它主要是用来解决shared_ptr的循环引用问题的。
# shared_ptr的循环引用问题
例如,定义如下的结点类:
struct ListNode {
ListNode* _next;
ListNode* _prev;
int _val;
~ListNode() {
cout << "~ListNode()" << endl;
}
};
int main() {
ListNode* node1 = new ListNode;
ListNode* node2 = new ListNode;
node1->_next = node2;
node2->_prev = node1;
//...
delete node1;
delete node2;
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
为了防止程序中途返回或抛异常等原因导致结点未被释放,我们将这两个结点分别交给两个shared_ptr对象进行管理,这时为了让连接节点时的赋值操作能够执行,就需要把ListNode类中的next和prev成员变量的类型也改为shared_ptr类型.
struct ListNode {
std::shared_ptr<ListNode> _next;
std::shared_ptr<ListNode> _prev;
int _val;
~ListNode() {
cout << "~ListNode()" << endl;
}
};
int main() {
std::shared_ptr<ListNode> node1(new ListNode);
std::shared_ptr<ListNode> node2(new ListNode);
node1->_next = node2;
node2->_prev = node1;
//...
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
这时程序运行结束后两个结点都没有被释放,但如果去掉连接结点时的两句代码中的任意一句,那么这两个结点就都能够正确释放,根本原因就是因为这两句连接结点的代码导致了循环引用。
- 当资源对应的引用计数减为0时对应的资源才会被释放,因此资源1的释放取决于资源2当中的prev成员,而资源2的释放取决于资源1当中的next成员。
- 而资源1当中的next成员的释放又取决于资源1,资源2当中的prev成员的释放又取决于资源2,于是这就变成了一个死循环,最终导致资源无法释放。
# weak_ptr解决循环引用问题
weak_ptr支持用shared_ptr对象来构造weak_ptr对象,构造出来的weak_ptr对象与shared_ptr对象管理同一个资源,但不会增加这块资源对应的引用计数。
将ListNode中的next和prev成员的类型换成weak_ptr就不会导致循环引用问题了,此时当node1和node2生命周期结束时两个资源对应的引用计数就都会被减为0,进而释放这两个结点的资源。比如:
struct ListNode {
std::weak_ptr<ListNode> _next;
std::weak_ptr<ListNode> _prev;
int _val;
~ListNode() {
cout << "~ListNode()" << endl;
}
};
int main() {
std::shared_ptr<ListNode> node1(new ListNode);
std::shared_ptr<ListNode> node2(new ListNode);
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
node1->_next = node2;
node2->_prev = node1;
//...
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21