C++内存列传之RAII宇宙:智能指针
文章目录
智能指针是 C++ 中用于自动管理动态内存的类模板,它通过 RAII(资源获取即初始化)技术避免手动 new / delete 操作,从而显著减少内存泄漏和悬空指针的风险
1.为什么需要智能指针?
intdiv(){int a, b; cin >> a >> b;if(b ==0)throwinvalid_argument("除0错误");return a / b;}voidFunc(){int* p1 =newint;int* p2 =newint; cout <<div()<< endl;delete p1;delete p2;}intmain(){try{Func();}catch(exception& e){ cout << e.what()<< endl;}return0;}如果 p1 这里 new 抛异常会如何?
p1未成功分配,值为nullptr
函数直接跳转到catch块,p2未分配,无内存泄漏
如果 p2 这里 new 抛异常会如何?
p1已分配但未释放,导致内存泄漏
函数跳转到catch块,p2未分配,delete p1和delete p2均未执行
如果 div 调用这里又会抛异常会如何?
p1和p2均已分配但未释放,导致双重内存泄漏
函数跳转到catch块,打印错误信息(如 “除0错误”)
C++ 不像 java 具有垃圾回收机制,能够自动回收开辟的空间,需要自行手动管理,但是自己管理有时又太麻烦了,况且这里只是两个指针就产生了这么多问题,因此在 C++11 就推出了智能指针用于自动管理内存
2.智能指针原理
2.1 RAll
template<classT>classSmartPtr{public:SmartPtr(T* ptr =nullptr):_ptr(ptr){}~SmartPtr(){if(_ptr)delete _ptr;}private: T* _ptr;};intmain(){ SmartPtr<int>sp1(newint(1)); SmartPtr<string>sp2(newstring("xxx"));return0;}RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术
简单来说,就是把创建的对象给到 SmartPtr 类来管理,当对象的生命周期结束的时候,刚好类也会自动调用析构函数进行内存释放
这种做法有两大好处:
- 不需要显式地释放资源
- 采用这种方式,对象所需的资源在其生命期内始终保持有效
2.2 像指针一样使用
都叫做智能指针了,那肯定是可以当作指针一样使用了,指针可以解引用,也可
以通过 -> 去访问所指空间中的内容,因此类中还得需要将 * 、-> 重载下,才可让其像指针一样去使用
template<classT>classSmartPtr{public:SmartPtr(T* ptr):_ptr(ptr){}~SmartPtr(){ cout <<"delete:"<< _ptr << endl;delete _ptr;} T&operator*(){return*_ptr;} T*operator->(){return _ptr;}private: T* _ptr;};* 重载返回对象,-> 重载返回地址,这部分的知识点在迭代器底层分析已经讲过很多遍了,就不过多叙述了,可自行翻阅前文
3.C++11的智能指针
智能指针一般放在 <memery> 文件里,C++11 也参考了第三方库 boost
C++ 98中产生了第一个智能指针auto_ptrC++ boost给出了更实用的scoped_ptr和shared_ptr和weak_ptrC++ TR1,引入了shared_ptr等。不过注意的是TR1并不是标准版C++ 11,引入了unique_ptr和shared_ptr和weak_ptr。需要注意的是unique_ptr对应boost的scoped_ptr。并且这些智能指针的实现原理是参考boost中的实现的
3.1 auto_ptr
template<classT>classauto_ptr{public:// RAII// 像指针一样auto_ptr(T* ptr):_ptr(ptr){}~auto_ptr(){ cout <<"delete:"<< _ptr << endl;delete _ptr;} T&operator*(){return*_ptr;} T*operator->(){return _ptr;}// ap3(ap1)// 管理权转移auto_ptr(auto_ptr<T>& ap):_ptr(ap._ptr){ ap._ptr =nullptr;} auto_ptr<T>&operator=(auto_ptr<T>& ap){if(this!=&ap){ _ptr = ap._ptr;// 转移所有权 ap._ptr =nullptr;// 原指针置空}return*this;}private: T* _ptr;};auto_ptr 在 C++98 就已经被引入,实现了智能指针如上面所讲的最基础的功能,同时他还额外对拷贝构造、= 重载进行了显式调用,但是这种拷贝虽然能解决新对象的初始化,但是对于被拷贝的对象,造成了指针资源所有权被转移走,跟移动构造有些类似
因此,auto_ptr 会导致管理权转移,拷贝对象被悬空,auto_ptr 是一个失败设计,很多公司明确要求不能使用 auto_ptr
3.2 unique_ptr
template<classT>classunique_ptr{public:// RAII// 像指针一样unique_ptr(T* ptr):_ptr(ptr){}~unique_ptr(){ cout <<"delete:"<< _ptr << endl;delete _ptr;} T&operator*(){return*_ptr;} T*operator->(){return _ptr;}// ap3(ap1)// 管理权转移// 防拷贝unique_ptr(unique_ptr<T>& ap)=delete; unique_ptr<T>&operator=(unique_ptr<T>& ap)=delete;private: T* _ptr;};unique_ptr 很简单粗暴,直接禁止了拷贝机制
因此,建议在不需要拷贝的场景使用该智能指针
3.3 shared_ptr
template<classT>classshared_ptr{public:// RAII// 像指针一样shared_ptr(T* ptr =nullptr):_ptr(ptr),_pcount(newint(1)){}~shared_ptr(){if(--(*_pcount)==0){ cout <<"delete:"<< _ptr << endl;delete _ptr;delete _pcount;}} T&operator*(){return*_ptr;} T*operator->(){return _ptr;}// sp3(sp1)shared_ptr(const shared_ptr<T>& sp):_ptr(sp._ptr),_pcount(sp._pcount){++(*_pcount);} shared_ptr<T>&operator=(const shared_ptr<T>& sp){if(_ptr == sp._ptr)return*this;if(--(*_pcount)==0){delete _ptr;delete _pcount;} _ptr = sp._ptr; _pcount = sp._pcount;++(*_pcount);return*this;}intuse_count()const{return*_pcount;} T*get()const{return _ptr;}private: T* _ptr;int* _pcount;};C++11 中的智能指针就属 shared_ptr 使用的最多,因为它解决了赋值造成的资源被转移可能会被错误访问的问题
类中增加一个新的指针 _pcount 用于计数,即计数有多少个 _ptr 指向同一片空间,多个 shared_ptr 可以同时指向同一个对象,每次创建新的 shared_ptr 指向该对象,引用计数加 1;每次 shared_ptr 析构或者被赋值为指向其他对象,引用计数减 1。当最后一个指向该对象的 shared_ptr 析构时,对象会被自动删除,从而避免内存泄漏
🔥值得注意的是:shared_ptr 同时也支持了无法自己给自己赋值,这里还涉及一些关于线程安全的知识点,待 Linux 学习过后再来补充
3.4 weak_ptr
看似完美的 shared_ptr 其实也会有疏漏,比如:引用循环
structListNode{int _data; shared_ptr<ListNode> _next; shared_ptr<ListNode> _prev;};intmain(){ shared_ptr<ListNode>node1(new ListNode); 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;return0;}当执行 node1->next = node2 和 node2->prev = node1 时,node1 内部的 _next 指针指向 node2 ,node2 内部的 _prev 指针指向 node1 。这就导致两个节点之间形成了循环引用关系。此时,由于互相引用,每个节点的引用计数都变为 2 ,因为除了外部的智能指针引用,还多了来自另一个节点内部指针的引用
当 node1 和 node2 智能指针对象离开作用域开始析构时,它们首先会将所指向节点的引用计数减 1 。此时,每个节点的引用计数变为 1 ,而不是预期的 0 。这是因为 node1 的 _next 还指向 node2 ,node2 的 _prev 还指向 node1 ,使得它们的引用计数无法归零
对于 shared_ptr 来说,只有当引用计数变为 0 时才会释放所管理的资源。由于这种循环引用的存在,node1 等待 node2 先释放(因为 node2 的 _prev 引用着 node1 ),而 node2 又等待 node1 先释放(因为 node1 的 _next 引用着 node2 ),最终导致这两个节点所占用的资源都无法被释放,造成内存泄漏
classListNode{public: weak_ptr<ListNode> _next; weak_ptr<ListNode> _prev;};为了解决 shared_ptr 的循环引用问题,通常可以使用 weak_ptr 。weak_ptr 是一种弱引用智能指针,它不会增加所指向对象的引用计数。将循环引用中的某一个引用(比如 ListNode 类中的 _prev 或 _next 其中之一)改为 weak_ptr 类型,就可以打破循环引用
因此,weak_ptr 是一种专门解决循环引用问题的指针
4.删除器
#include<iostream>#include<memory>#include<string>usingnamespace std;classA{public:~A(){ cout <<"A::~A()"<< endl;}};// 仿函数删除器:用于释放malloc分配的内存template<classT>structFreeFunc{voidoperator()(T* ptr)const{ cout <<"FreeFunc: free memory at "<< ptr << endl;free(ptr);}};// 仿函数删除器:用于释放数组template<classT>structDeleteArrayFunc{voidoperator()(T* ptr)const{ cout <<"DeleteArrayFunc: delete[] memory at "<< ptr << endl;delete[] ptr;}};intmain(){// 使用FreeFunc删除器的shared_ptr shared_ptr<int>sp1((int*)malloc(sizeof(int)),FreeFunc<int>());*sp1 =100; cout <<"sp1: "<<*sp1 <<" at "<< sp1.get()<< endl;// 离开作用域时调用FreeFunc删除器// 使用DeleteArrayFunc删除器的shared_ptr shared_ptr<int>sp2(newint[5],DeleteArrayFunc<int>());for(int i =0; i <5;++i){ sp2.get()[i]= i;} cout <<"sp2 array:";for(int i =0; i <5;++i){ cout <<" "<< sp2.get()[i];} cout << endl;// 离开作用域时调用DeleteArrayFunc删除器// 使用lambda删除器管理A对象数组 shared_ptr<A>sp4(new A[3],[](A* p){ cout <<"Lambda: deleting array at "<< p << endl;delete[] p;}); cout <<"sp4 array of A objects created"<< endl;// 离开作用域时调用lambda删除器// 使用lambda删除器管理文件句柄 shared_ptr<FILE>sp5(fopen("test.txt","w"),[](FILE* p){if(p){ cout <<"Lambda: closing file"<< endl;fclose(p);}});if(sp5){fprintf(sp5.get(),"Hello, shared_ptr with deleter!\n"); cout <<"File written"<< endl;}// 离开作用域时调用lambda删除器关闭文件return0;}对于所有的指针不一定是 new 出来的对象,因此利用仿函数设置了删除器,这样就可以调用对应的删除
希望读者们多多三连支持
小编会继续更新
你们的鼓励就是我前进的动力!
