赞
踩
「前言」文章是关于C++11的智能指针方面
「归属专栏」C嘎嘎
「主页链接」个人主页
「笔者」枫叶先生(fy)
关于内存泄漏的问题,例如下面的代码:
- #include <iostream>
- using namespace std;
-
- int div()
- {
- int a, b;
- cin >> a >> b;
- if (b == 0)
- throw invalid_argument("除0错误");
- return a / b;
- }
- void Func()
- {
- 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;
- }
![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)
执行上述代码时,如果输入的除数为0,那么div函数中就会抛出异常,这时程序的执行流会直接跳转到主函数中的 catch块中执行,最终导致 Func函数中申请的内存资源没有得到释放
对于这种情况,我们可以在 Func函数中先对 div函数中抛出的异常进行捕获,捕获后先将之前申请的内存资源释放,然后再将异常重新抛出,代码如下
- int div()
- {
- int a, b;
- cin >> a >> b;
- if (b == 0)
- throw invalid_argument("除0错误");
- return a / b;
- }
- void Func()
- {
- int* p1 = new int;
- int* p2 = new int;
- try
- {
- cout << div() << endl;
- }
- catch (...)
- {
- delete p1;
- delete p2;
- throw;
- }
- delete p1;
- delete p2;
- }
- int main()
- {
- try
- {
- Func();
- }
- catch (exception& e)
- {
- cout << e.what() << endl;
- }
- return 0;
- }
![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)
new也会抛异常的,如果p1这里new 抛异常,这时程序的执行流会直接跳转到主函数中的 catch块中执行,这里没有问题。假设p1没有问题,那如果p2的 new 也抛异常呢?需要再嵌套一个 try、catch??
- void Func()
- {
- int* p1 = new int;
- int* p2 = nullptr;
- try
- {
- int* p2 = new int;//p2的new可能会抛异常
- try
- {
- cout << div() << endl;
- }
- catch (...)
- {
- delete p1;
- delete p2;
- throw;
- }
- }
- catch (...)
- {
- delete p1;
- throw;
- }
- delete p1;
- delete p2;
- }
![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)
这样的代码显得很low,疯狂嵌套try、catch语句,所以为了解决这个问题就出现的智能指针。
补充:内存泄漏分类,C/C++程序中一般我们关心两种方面的内存泄漏:
内存泄漏非常常见,解决方案分为两种:1、事前预防型。如智能指针等。2、事后查错型。如泄漏检测工具
什么是智能指针??
*
和 ->
运算符进行重载例如:
- //智能指针
- 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> sp1(new int);
- SmartPtr<int> sp2(new int);
- cout << div() << endl;
- }
-
- int main()
- {
- try
- {
- Func();
- }
- catch (exception& e)
- {
- cout << e.what() << endl;
- }
- return 0;
- }
![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)
上面的代码中将申请到的内存空间交给了一个SmartPtr的对象 sp1、sp2进行管理,这样一来,无论程序是正常执行完毕返回了,还是因为某些原因中途返回了,或是因为抛异常返回了,只要SmartPtr对象的生命周期结束就会调用其对应的析构函数,进而完成内存资源的释放。
(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;
- };
(2)像指针行为
上述(1)的SmartPtr还不能将其称为智能指针,因为它还不具有指针的行为。指针可以解引用,也可以通过->去访问所指空间中的内容,因此:智能指针中还得需要将 * 、-> 重载下,才可让其像指针一样去使用
- 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;
- };
![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)
总结一下智能指针的原理:
但是这样的智能指针还不够完善,会存在智能指针对象拷贝的问题,所以C++出现了不同版本的智能指针
决智能指针对象的拷贝问题:比如上面实现的智能指针 SmartPtr类,如果用一个SmartPtr对象来拷贝构造另一个SmartPtr对象,或是将一个SmartPtr对象赋值给另一个SmartPtr对象,都会导致程序崩溃
- int main()
- {
- SmartPtr<int> sp1(new int);
- SmartPtr<int> sp2(sp1); //拷贝构造
-
- SmartPtr<int> sp3(new int);
- SmartPtr<int> sp4(new int);
- sp3 = sp4; //拷贝赋值
-
- return 0;
- }
原因:
C++98版本的库中就提供了 auto_ptr 的智能指针,文档介绍:std::auto_ptr
- 头文件:
- #include <memory>
auto_ptr 通过管理权转移的方式解决智能指针的拷贝问题,保证一个资源在任何时刻都只有一个对象在对其进行管理,这时同一个资源就不会被多次释放了
测试代码如下:
- int main()
- {
- std::auto_ptr<int> ap1(new int(1));
- std::auto_ptr<int> ap2(ap1);
- *ap2 = 10;
-
- std::auto_ptr<int> ap3(new int(1));
- std::auto_ptr<int> ap4(new int(2));
- ap3 = ap4;
-
- return 0;
- }
进行调试查看:
对一个对象的管理权转移后也就意味着,该对象不能再用对原来管理的资源进行访问了,会造成对象悬空,比如上面的 sp1、sp2,继续使用这两个对象程序就会直接崩溃,因此使用 auto_ptr之前必须先了解它的机制,否则程序很容易出问题。
auto_ptr是一个失败设计,很多公司也都明确规定了禁止使用auto_ptr
auto_ptr的简单模拟实现
- namespace fy
- {
- template<class T>
- class auto_ptr
- {
- public:
- //RAII
- 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;
- };
- }
![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)
unique_ptr是C++11中引入的智能指针,unique_ptr通过防拷贝的方式解决智能指针的拷贝问题,也就是简单粗暴的防止对智能指针对象进行拷贝,这样也能保证资源不会被多次释放
- 头文件:
- #include <memory>
文档介绍:unique_ptr
测试代码
- int main()
- {
- std::unique_ptr<int> up1(new int(1));
- std::unique_ptr<int> up2(up1); //error,不允许拷贝
- return 0;
- }
编译报错
unique_ptr简单模拟实现
- namespace fy
- {
- template<class T>
- class unique_ptr
- {
- public:
- // RAII
- 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;
- };
- }
![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)
shared_ptr是 C++11中引入的智能指针,shared_ptr通过引用计数的方式解决智能指针的拷贝问题,也就是说shared_ptr支持拷贝
- 头文件:
- #include <memory>
文档介绍:shared_ptr
shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源
通过这种引用计数的方式就能支持多个对象一起管理某一个资源,也就是支持了智能指针的拷贝,并且只有当一个资源对应的引用计数减为0时才会释放资源,因此保证了同一个资源不会被释放多次
测试代码:
- // 引用计数支持多个拷贝管理同一个资源,最后一个析构对象释放资源
- int main()
- {
- shared_ptr<int> sp1(new int(1));
- shared_ptr<int> sp2(sp1);
- *sp1 = 2;
- *sp2 = 3;
- //use_count成员函数,用于获取当前对象管理的资源对应的引用计数
- cout << sp1.use_count() << endl;
-
- shared_ptr<int> sp3(new int(1));
- shared_ptr<int> sp4(new int(2));
- shared_ptr<int> sp5(new int(3));
- sp4 = sp3;
- sp5 = sp3;
- cout << sp3.use_count() << endl;
- return 0;
- }
![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)
运行结果
调试查看
shared_ptr简单模拟实现
- namespace fy
- {
- template <class T>
- class shared_ptr
- {
- public:
- // (1)RAII
- shared_ptr(T *ptr)
- : _ptr(ptr), _pCount(new int(1))
- {
- }
- ~shared_ptr()
- {
- if (--(*_pCount) == 0)
- {
- if (_ptr != nullptr)
- {
- std::cout << "_ptr: " << _ptr << std::endl;
- delete _ptr;
- _ptr = nullptr;
- }
- delete _pCount;
- _pCount = nullptr;
- }
- }
- shared_ptr(const shared_ptr<T> &sp)
- : _ptr(sp._ptr), _pCount(sp._pCount)
- {
- ++(*_pCount);
- }
-
- shared_ptr<T> &operator=(shared_ptr<T> &sp)
- {
- if (_ptr != sp._ptr) // 管理同一块空间的对象之间无需进行赋值操作
- {
- if (--(*_pCount) == 0)
- {
- std::cout << "operator= delete: " << _ptr << std::endl;
- delete _ptr;
- delete _pCount;
- }
- _ptr = sp._ptr;
- _pCount = sp._pCount;
- ++(*_pCount);
- }
- return *this;
- }
- // 获取引用计数
- int use_count()
- {
- return *_pCount;
- }
- // (2)可以像指针一样使用
- T &operator*()
- {
- return *_ptr;
- }
- T *operator->()
- {
- return _ptr;
- }
-
- private:
- T *_ptr; // 管理的资源
- int *_pCount; // 管理的资源对应的引用计数
- };
- }
![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)
测试运行
注意:shared_ptr中的引用计数count不能单纯的定义成一个int类型的成员变量,因为这就意味着每个shared_ptr对象都有一个自己的count成员变量,而当多个对象要管理同一个资源时,这几个对象应该用到的是同一个引用计数
上面模拟实现的shared_ptr还存在线程安全的问题,由于管理同一个资源的多个对象的引用计数是共享的,因此多个线程可能会同时对同一个引用计数进行自增或自减操作,而自增和自减操作都不是原子操作,因此需要通过加锁来对引用计数进行保护,否则就会导致线程安全问题
所以需要对代码中的 ++ -- 操作进行加锁
修改代码如下:
- #pragma once
- #include <iostream>
- #include <memory>
- #include <mutex>
-
- namespace fy
- {
- template <class T>
- class shared_ptr
- {
- public:
- // (1)RAII
- shared_ptr(T *ptr)
- : _ptr(ptr), _pCount(new int(1)), _mutex(new mutex)
- {
- }
- // 对++操作进行加锁
- void Add()
- {
- _mutex->lock();
- (*_pCount)++;
- _mutex->unlock();
- }
- // 对--操作进行加锁
- void Release()
- {
- _mutex->lock();
- bool flag = false;
- if (--(*_pCount) == 0) // 将管理的资源对应的引用计数--
- {
- if (_ptr != nullptr)
- {
- cout << "delete: " << _ptr << endl;
- delete _ptr;
- delete _pCount;
- _ptr = nullptr;
- _pCount = nullptr;
- flag = true;
- }
- }
- _mutex->unlock();
- if (flag == true) // 释放锁
- {
- delete _mutex;
- }
- }
- ~shared_ptr()
- {
- Release();
- }
- shared_ptr(const shared_ptr<T> &sp)
- : _ptr(sp._ptr), _pCount(sp._pCount), _mutex(sp._mutex)
- {
- Add();
- }
-
- shared_ptr<T> &operator=(shared_ptr<T> &sp)
- {
- if (_ptr != sp._ptr) // 管理同一块空间的对象之间无需进行赋值操作
- {
- Release();
- _ptr = sp._ptr;
- _pCount = sp._pCount;
- _mutex = sp._mutex;
- Add();
- }
- return *this;
- }
- // 获取引用计数
- int use_count()
- {
- return *_pCount;
- }
- // (2)可以像指针一样使用
- T &operator*()
- {
- return *_ptr;
- }
- T *operator->()
- {
- return _ptr;
- }
-
- private:
- T *_ptr; // 管理的资源
- int *_pCount; // 管理的资源对应的引用计数
- mutex *_mutex; // 管理的资源对应的互斥锁
- };
- }
![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)
智能指针的定制删除器
当智能指针对象的生命周期结束时,所有的智能指针默认都是以delete
的方式将资源释放,这是不太合适的,因为智能指针并不是只管理以new
方式申请到的内存空间,智能指针管理的也可能是以new[]
的方式申请到的空间,或管理的是一个文件指针,以new[]
的方式申请到的内存空间必须以delete[]
的方式进行释放,而文件指针必须通过调用fclose
函数进行释放
定制删除器是通过仿函数、Lambda表达式或函数指针来实现的,这里简单了解一下,定制删除器是实现很复杂。
shared_ptr存在一个致命的缺陷:循环引用,为了解决这个问题,产生了weak_ptr。 weak_ptr是C++11中引入的智能指针,weak_ptr不是用来管理资源的释放的,weak_ptr 是对 shared_ptr的补充。
- 头文件:
- #include <memory>
文档介绍:weak_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;
- }
![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)
上述程序是没有问题的,两个结点都能够正确释放。为了防止程序中途返回或抛异常等原因导致结点未被释放,我们将这两个结点分别交给两个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;
- }
![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)
这时程序运行结束后两个结点都没有被释放,但如果去掉连接结点时的两句代码中的任意一句,那么这两个结点就都能够正确释放,根本原因就是因为这两句连接结点的代码导致了循环引用
循环引用分析:
weak_ptr是C++11中引入的智能指针,weak_ptr不是用来管理资源的释放的,它主要是用来解决shared_ptr的循环引用问题的
在引用计数的场景下,把节点中的_prev和_next改成weak_ptr就可以了,原理就是,node1->_next = node2;和node2->_prev = node1;时weak_ptr的_next和_prev不会增加node1和node2的引用计数。
修改代码如下:
- 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;
- }
![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)
编译运行,资源正常释放
weak_ptr简单模拟实现
shared_ptr还会提供一个get函数,用于获取其管理的资源,辅助weak_ptr
- T* get() const
- {
- return _ptr;
- }
weak_ptr模拟实现代码如下:
- template<class T>
- class weak_ptr
- {
- public:
- // RAII
- weak_ptr()
- :_ptr(nullptr)
- {}
- weak_ptr(const shared_ptr<T>& sp)
- :_ptr(sp.get())//weak_ptr支持用shared_ptr对象拷贝构造weak_ptr对象,构造时获取shared_ptr对象管理的资源。
- {}
- weak_ptr<T>& operator=(const shared_ptr<T>& sp)
- {
- _ptr = sp.get();//weak_ptr支持用shared_ptr对象拷贝赋值给weak_ptr对象,赋值时获取shared_ptr对象管理的资源
- return *this;
- }
- // (2)可以像指针一样使用
- T& operator*()
- {
- return *_ptr;
- }
- T* operator->()
- {
- return _ptr;
- }
- private:
- T* _ptr;
- };
![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)
--------------------- END ----------------------
- 「 作者 」 枫叶先生
- 「 更新 」 2023.5.13
- 「 声明 」 余之才疏学浅,故所撰文疏漏难免,
- 或有谬误或不准确之处,敬请读者批评指正。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。