文章目录
- 1. 前言
- 2. 为什么需要智能指针?
- 3. 内存泄漏
- 3.1 内存泄漏的概念及危害
- 3.2 内存泄漏的分类
- 3.3 如何检测内存泄漏
- 3.4 如何避免内存泄漏
- 4. 智能指针的使用及原理
- 4.1 RAII思想
- 4.2 智能指针的原理
- 4.3 C++智能指针发展历史
- 4.4 std::auto_ptr
- 4.5 std::unique_ptr
- 4.6 std::shared_ptr
- 4.7 shared_ptr的循环引用问题
- 4.8 定制删除器
1. 前言
C++提供了智能指针的概念,它可以自动管理动态分配的内存资源。智能指针是一种对象,它表现得像一个常规指针,但具有附加的内存管理功能。本篇文章将着重讲解智能指针的作用、内存泄漏以及智能指针的使用和原理。
2. 为什么需要智能指针?
在某些场景下,我们在堆上申请了一块空间,但是却无法正常释放。
下面我们先来分析下面这段程序有没有什么内存方面的问题?
int div() {int a, b;cin >> a >> b;if (b == 0)throw invalid_argument("除0错误");return a / b; } void Func() {// 1、如果p1这里new 抛异常会如何?// 2、如果p2这里new 抛异常会如何?// 3、如果div调用这里又会抛异常会如何?int* p1 = new int;int* p2 = new int;cout << div() << endl;delete p1;delete p2; } int main() {try{Func();}catch (exception& e){cout << e.what() << endl;}return 0; }
问题说明:
- 如果在
p1
这里使用new
分配内存时抛出异常,那么异常将传播到Func()
函数的调用点。由于没有捕获该异常的代码,异常会继续传播到main()
函数中的异常处理部分。此时,p1
指针没有被成功分配内存,因此不会发生内存泄漏。在异常处理部分,您可以选择适当地清理和处理异常情况。- 如果在
p2
这里使用new
分配内存时抛出异常,与第一个问题类似,异常也会传播到Func()
函数的调用点。同样地,没有捕获该异常的代码,它会继续传播到main()
函数中的异常处理部分。此时,p1
指针已成功分配内存,但p2
指针没有成功分配内存。因此,在异常处理部分中,您需要确保释放已分配的内存(delete p1
)并处理异常情况。- 如果在
div()
函数中抛出异常,它会立即终止函数的执行,并且异常会传播到调用div()
的地方,也就是Func()
函数的调用点。同样地,由于没有对该异常进行捕获,它会继续传播到main()
函数中的异常处理部分。在此过程中,p1
和p2
指针已成功分配内存,因此您需要确保在异常处理部分中释放已分配的内存(delete p1
和delete p2
)并处理异常情况。在异常处理部分,我们可以选择清理资源、记录日志或向用户显示错误信息等。同时,确保释放已分配的内存,以避免内存泄漏。在使用
new
分配内存时,可以使用智能指针(如std::unique_ptr
或std::shared_ptr
)来管理动态内存,而不是手动使用new
和delete
。智能指针可以自动处理内存释放,避免了手动释放内存的繁琐和容易出错的过程。
3. 内存泄漏
3.1 内存泄漏的概念及危害
什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。
以下是两种内存泄漏的情况:
void MemoryLeaks() {// 1.内存申请了忘记释放int* p1 = (int*)malloc(sizeof(int));int* p2 = new int;// 2.异常安全问题int* p3 = new int[10];Func(); // 这里Func函数抛异常导致 delete[] p3未执行,p3没被释放delete[] p3; }
3.2 内存泄漏的分类
C/C++程序中一般我们关心两种方面的内存泄漏:
堆内存泄漏(Heap leak)
堆内存指的是程序执行中依据须要分配通过
malloc/calloc/realloc/new
等从堆中分配的一块内存,用完后必须通过调用相应的free
或者delete
删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。系统资源泄漏
指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。
3.3 如何检测内存泄漏
在linux下内存泄漏检测:Linux下几款内存泄露检查工具。
在windows下使用第三方工具:VLD(Visual LeakDetector)内存泄露库。
其他工具:内存泄漏工具比较:内存泄露检测工具比较。
3.4 如何避免内存泄漏
工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。(这个理想状态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。需要下一条智能指针来管理才有保证。)
采用RAII思想或者智能指针来管理资源。
有些公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。
出问题了使用内存泄漏工具检测。(不过很多工具都不够靠谱,或者收费昂贵。)
总结:
内存泄漏非常常见,解决方案分为两种:1、事前预防型。如智能指针等。2、事后查错型。如泄漏检测工具。
4. 智能指针的使用及原理
4.1 RAII思想
RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:
- 不需要显式地释放资源。
- 采用这种方式,对象所需的资源在其生命期内始终保持有效。
使用RAII思想设计的SmartPtr类
template<class T> class SmartPtr { public:SmartPtr(T* ptr = nullptr): _ptr(ptr){}~SmartPtr(){if (_ptr)delete _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> sp1(new int);SmartPtr<int> sp2(new int);cout << div() << endl; } int main() {try {Func();}catch (const exception& e){cout << e.what() << endl;}return 0; }
4.2 智能指针的原理
上述的
SmartPtr
还不能将其称为智能指针,因为它还不具有指针的行为。指针可以解引用,也可以通过->
去访问所指空间中的内容,因此,AutoPtr
模板类中还得需要将*
、->
重载下,才可让其像指针一样去使用。template<class T> class SmartPtr { public:SmartPtr(T* ptr = nullptr): _ptr(ptr){}~SmartPtr(){if (_ptr)delete _ptr;}T& operator*() { return *_ptr; }T* operator->() { return _ptr; } private:T* _ptr; }; struct Date {int _year;int _month;int _day; }; int main() {SmartPtr<int> sp(new int);*sp = 10;SmartPtr<Date> sparray(new Date);sparray->_year = 2024;sparray->_month = 1;sparray->_day = 1;return 0; }
智能指针的原理:
- RAII特性。
- 重载
operator*
和opertaor->
,具有像指针一样的行为。
4.3 C++智能指针发展历史
思考一下,前面我们简单实现的智能指针存在一个大坑。那就是当一个指针拷贝构造或者赋值给另外一个指针时,由于我们想让两个指针指向同一块空间,所以我们实现的是浅拷贝,但是指向了同一块空间就会有析构函数调用两次的风险。由于这一个大坑,智能指针进行了很多次优化更迭。
- 在C++98的时候其实就已经在库中实现了智能指针,也就是第一个智能指针**
auto_ptr
**。
- 既然有析构两次的风险,那么
auto_ptr
在每一次拷贝构造或赋值后直接将自己置空,这样实现了管理权转移。但是对于不了解auto_ptr
的人来说,使用它无疑是巨大的风险。
- 因此,在很长一段时间里,程序员都习惯于使用boost库中的智能指针(
scoped_ptr
、shared_ptr
和weak_ptr
)。- C++11在boost库的基础上完善并推出了
unique_ptr
、shared_ptr
和weak_ptr
,这与以上boost库中的三个智能指针相对应。接下来我们来看看这些智能指针的功能和区别。
4.4 std::auto_ptr
std::auto_ptr文档
C++98版本的库中就提供了
auto_ptr
的智能指针。下面演示auto_ptr
的使用及问题。
auto_ptr
的实现原理:管理权转移的思想,下面简化模拟实现了一份my_ptr::auto_ptr
来了解它的原理。// C++98 管理权转移 auto_ptr namespace my_ptr {template<class T>class auto_ptr{public:auto_ptr(T* ptr):_ptr(ptr){}auto_ptr(auto_ptr<T>& sp):_ptr(sp._ptr){// 管理权转移sp._ptr = nullptr;}auto_ptr<T>& operator=(auto_ptr<T>& ap){// 检测是否为自己给自己赋值if (this != &ap){// 释放当前对象中资源if (_ptr)delete _ptr;// 转移ap中资源到当前对象中_ptr = ap._ptr;ap._ptr = NULL;}return *this;}~auto_ptr(){if (_ptr){cout << "delete:" << _ptr << endl;delete _ptr;}}// 像指针一样使用T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;}; }int main() {std::auto_ptr<int> sp1(new int);std::auto_ptr<int> sp2(sp1); // 管理权转移// sp1悬空*sp2 = 10;cout << *sp2 << endl;cout << *sp1 << endl; // errorreturn 0; }
auto_ptr
是一个失败设计,很多公司明确要求不能使用auto_ptr
。
4.5 std::unique_ptr
unique_ptr文档
unique_ptr
的实现原理:独占所有权,简单粗暴的防拷贝,下面简化模拟实现了一份my_ptr::unique_ptr
来了解它的原理。namespace my_ptr {template<class T>class unique_ptr{public:unique_ptr(T* ptr):_ptr(ptr){}~unique_ptr(){if (_ptr){cout << "delete:" << _ptr << endl;delete _ptr;}}// 像指针一样使用T& operator*(){return *_ptr;}T* operator->(){return _ptr;}unique_ptr(const unique_ptr<T>&sp) = delete;unique_ptr<T>& operator=(const unique_ptr<T>&sp) = delete;private:T* _ptr;}; } int main() {my_ptr::unique_ptr<int> sp1(new int);//my_ptr::unique_ptr<int> sp2(sp1); // errorstd::unique_ptr<int> sp1(new int);//std::unique_ptr<int> sp2(sp1); // errorreturn 0; }
unique_ptr
直接禁用了拷贝和赋值,独占指针所有权,在一些场景下,还是可以用到它的。
4.6 std::shared_ptr
std::shared_ptr文档
shared_ptr
是实际运用中使用最多的智能指针,通过引用计数支持拷贝赋值,非常优雅!
shared_ptr
的原理:是通过引用计数的方式来实现多个shared_ptr
对象之间共享资源。
shared_ptr
在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。
如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源。
如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。
namespace my_ptr {template<class T>class shared_ptr{public:// RAIIshared_ptr(T* ptr = nullptr):_ptr(ptr), _pcount(new int(1)){}void release(){if (--(*_pcount) == 0){//cout << "delete->" << _ptr << endl;delete _ptr;delete _pcount;}}~shared_ptr(){release();}shared_ptr(const shared_ptr<T>& sp):_ptr(sp._ptr), _pcount(sp._pcount){++(*_pcount);}// sp1 = sp3shared_ptr<T>& operator=(const shared_ptr<T>& sp){if (_ptr != sp._ptr){release();_ptr = sp._ptr;_pcount = sp._pcount;++(*_pcount);}return *this;}// 像指针一样T& operator*(){return *_ptr;}T* operator->(){return _ptr;}int use_count() const{return *_pcount;}T* get() const{return _ptr;}private:T* _ptr;int* _pcount;}; }
4.7 shared_ptr的循环引用问题
请先分析以下代码:
struct ListNode {int _data;my_ptr::shared_ptr<ListNode> _prev;my_ptr::shared_ptr<ListNode> _next;~ListNode() { cout << "~ListNode()" << endl; } }; int main() {my_ptr::shared_ptr<ListNode> node1(new ListNode);my_ptr::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; }
输出结果:
可以看到这里并未调用
ListNode
的析构函数,为什么呢?循环引用分析:
node1
和node2
两个智能指针对象指向两个节点,引用计数变成1,我们不需要手动delete。
node1
的_next
指向node2
,node2
的_prev
指向node1
,引用计数变成2。
node1
和node2
析构,引用计数减到1,但是_next
还指向下一个节点。但是_prev
还指向上一个节点。也就是说
_next
析构了,node2
就释放了。也就是说
_prev
析构了,node1
就释放了。但是
_next
属于node1
的成员,node1
释放了,_next
才会析构,而node1
由_prev
管理,_prev
属于node2
成员,所以这就叫循环引用,谁也不会释放。图解:
那么我们如何解决此问题呢?
这里我们就来认识一下专门用来配合
shared_ptr
使用的指针——weak_ptr
。先来看一下它的简单实现版本:
namespace my_ptr {// 简化版本的weak_ptr实现template<class T>class weak_ptr{public:weak_ptr():_ptr(nullptr){}weak_ptr(const shared_ptr<T>& sp):_ptr(sp.get()){}weak_ptr<T>& operator=(const shared_ptr<T>& sp){_ptr = sp.get();return *this;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;}; }
可以看到
weak_ptr
用于获得shared_ptr
管理的资源的非拥有访问权。通过weak_ptr
,可以检查资源是否存在,weak_ptr
不会增加资源的引用计数。因此上述问题的解决方案就是:在引用计数的场景下,把节点中的
_prev
和_next
改成weak_ptr
就可以了。struct ListNode {int _data;my_ptr::weak_ptr<ListNode> _prev;my_ptr::weak_ptr<ListNode> _next;~ListNode() { cout << "~ListNode()" << endl; } }; int main() {my_ptr::shared_ptr<ListNode> node1(new ListNode);my_ptr::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; }
这样
node1->_next = node2;
和node2->_prev = node1;
时weak_ptr
的_next
和_prev
不会增加node1
和node2
的引用计数,很好的解决了这里的问题。
4.8 定制删除器
当使用智能指针时,会出现下面问题:
shared_ptr<int> sp1(new int[10]);
当调用
sp1
的析构函数时,默认调用的是delete
,而new []
应该调用delete []
,我们该如何告诉编译器要使用的delete
方式呢?
shared_ptr
的构造函数有一个模板参数类型D可以让我们传参要调用的删除方法。因此,我们可以这样使用:
shared_ptr<int> sp2(new int[10], [](int* ptr) { delete[] ptr; }); shared_ptr<FILE> sp3(fopen("test.txt", "r"), [](FILE* ptr) { fclose(ptr); });
拓展
除了会用,让我们想想
shared_ptr
该如何实现?我们可以通过function包装器来定义成员函数去接收构造函数中的删除器参数。
namespace my_ptr {template<class T>class shared_ptr{public:// RAIIshared_ptr(T* ptr = nullptr):_ptr(ptr), _pcount(new int(1)){}template<class D>shared_ptr(T* ptr, D del): _ptr(ptr), _pcount(new int(1)), _del(del){}// function<void(T*)> _del;void release(){if (--(*_pcount) == 0){//cout << "delete->" << _ptr << endl;//delete _ptr;_del(_ptr);delete _pcount;}}~shared_ptr(){release();}shared_ptr(const shared_ptr<T>& sp):_ptr(sp._ptr), _pcount(sp._pcount){++(*_pcount);}// sp1 = sp3shared_ptr<T>& operator=(const shared_ptr<T>& sp){if (_ptr != sp._ptr){release();_ptr = sp._ptr;_pcount = sp._pcount;++(*_pcount);}return *this;}// 像指针一样T& operator*(){return *_ptr;}T* operator->(){return _ptr;}int use_count() const{return *_pcount;}T* get() const{return _ptr;}private:T* _ptr;int* _pcount;function<void(T*)> _del = [](T* ptr) {delete ptr; };}; }