【C++】unique_ptr、shared_ptr、weak_ptr,傻傻分不清楚

⭐️个人主页:@小羊⭐️所属专栏:C++很荣幸您能阅读我的文章,诚请评论指点,欢迎欢迎 ~

目录
前言
什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。
解决内存泄漏的问题,使用智能指针管理是一个很好的选择。
一、RAII
RAII(Resource Acquisition Is Initialization)是C++中的一种资源管理技术,其核心思想是利用对象的生命周期来自动管理资源,因为对象的构造和析构是自动调用的。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:
- 不需要显式地释放资源
- 对象所需的资源在其生命期内始终保持有效
不管对象是生命周期正常结束还是抛了异常,最后都会自动释放掉:

//使用RAII思想设计的SmartPtr类template<classT>classSmartPtr{public:SmartPtr(T* ptr =nullptr):_ptr(ptr){}~SmartPtr(){if(_ptr)delete _ptr;}private: T* _ptr;};二、智能指针原理
上面的SmartPtr还不能称为智能指针,因为它还不具有指针的行为。指针可以解引用,也可以通过->去访问所指空间中的内容,因此智能指针模板类中还得需要将* 、->重载下,才可让其像指针一样去使用。
template<classT>classSmartPtr{public:SmartPtr(T* ptr =nullptr):_ptr(ptr){}~SmartPtr(){if(_ptr)delete _ptr;} T&operator*(){return*_ptr;} T*operator->(){return _ptr;}private: T* _ptr;};structDate{int _year =1;int _month =1;int _day =1;};intmain(){ SmartPtr<int>sp1(newint);*sp1 =10; cout <<*sp1 << endl; SmartPtr<Date>sp2(new Date); sp2->_year =2024; sp2->_month =10; sp2->_day =25;}智能指针的原理:RAII特性+重载operator*和operator->,具有像指针一样的行为这么看智能指针好像还挺好理解挺简单的,不过先别高兴太早,智能指针麻烦的地方不在这里,在拷贝构造。
我们知道C++默认构造函数实现的是浅拷贝,而智能指针模拟的是原生指针的行为,我们期望它的拷贝就是浅拷贝,看似类的默认构造函数就很好的满足我们的需求,但是不要忘了让多个指针指向同一块空间,这样会导致同一块空间出现析构多次的情况,显然这里出现了矛盾。
三、auto_ptr
- C++智能指针都在头文件
<memory>中定义。
auto_ptr要求其对“裸”指针的完全占有性,即一个“裸”指针不能同时被两个以上的auto_ptr所拥有。
虽然auto_ptr提供了拷贝,但它的拷贝是一种管理权转移。被拷贝对象会失去资源的所有权,拷贝对象会接管这个资源。同样的赋值也会转移管理权。

//拷贝构造auto_ptr(auto_ptr<T>& ap):_ptr(ap._ptr){//管理权转移 ap._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存在上述限制和潜在问题,C++11及以后的版本引入了更先进的智能指针,如std::unique_ptr和std::shared_ptr,它们提供了更强大和灵活的资源管理功能。因此,在现代C++编程中,建议使用这些新的智能指针来替代auto_ptr。
四、unique_ptr
unique_ptr不支持拷贝,其拷贝构造函数被delete禁掉了。

unique_ptr(const unique_ptr<T>& sp)=delete; unique_ptr<T>&operator=(const unique_ptr<T>& sp)=delete;//不支持拷贝 unique_ptr<Date>up1(new Date); unique_ptr<Date>up2(up1);如果unique_ptr管理的是多个连续的空间,则释放时会出错,因为它的底层是delete,而释放连续的空间需要delete[]。

解决这个问题可以通过定制删除器来解决。

定制删除器:
template<classT>classDeleteArray{public:voidoperator()(T* ptr){delete[] ptr;}};classFclose{public:voidoperator()(FILE* pf){ cout <<"void operator()(FILE* pf)"<< endl;fclose(pf);}};intmain(){ unique_ptr<Date, DeleteArray<Date>>up1(new Date[10]); unique_ptr<FILE, Fclose>up2(fopen("text.txt","w"));return0;}
五、shared_ptr
我们重点学习shared_ptr。shared_ptr的原理:通过引用计数的方式来实现多个shared_ptr对象之间共享资源。
shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享- 在对象被销毁时,就说明自己不使用该资源了,对象的引用计数减一
- 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源
- 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了

同样的 shared_ptr 也是默认使用 delete,但资源可能是:连续空间、文件句柄(如 FILE*)、数据库连接、网络套接字、锁,因此也需要定制删除器,但它们两个支持传定制删除器的位置有所不同。

为什么unique_ptr和shared_ptr传定制删除器的位置不同?
- 存储方式
- shared_ptr:删除器不是类型的一部分,而是通过构造函数参数动态绑定,存储在控制块中。无论删除器如何,
shared_ptr的类型始终是std::shared_ptr<T>,因此不同删除器的shared_ptr可以互相赋值或传递。 - unique_ptr:删除器是类型的一部分,通过模板参数指定,存储在智能指针对象内部。不同删除器的
unique_ptr是不同类型,不能直接互相赋值或传递 。例如std::unique_ptr<FILE, decltype(&fclose)> file(fopen("test.txt", "r"), &fclose);。
- shared_ptr:删除器不是类型的一部分,而是通过构造函数参数动态绑定,存储在控制块中。无论删除器如何,
- 灵活性与类型擦除
- shared_ptr:删除器通过类型擦除实现,灵活性更高,可以捕获任意可调用对象(如函数、lambda、仿函数),无需在类型中显式声明。
- unique_ptr:删除器是类型的一部分,灵活性较低,需要显式指定模板参数。
unique_ptr的删除器写在模板中的原因
unique_ptr设计目标是实现独占拥有权,即任意时刻只能有一个unique_ptr拥有某个对象,那么将删除器作为模板参数,使删除器成为类型的一部分,就保证了不同删除器的unique_ptr类型不同,增强类型安全性。
另外,除了直接new对象,也可以用make_shared构造对象。它是一个函数模板:


make_shared一次性完成了对 控制块和被智能指针管理对象的 内存分配和对象构造,因此可以减少内存分配的次数,还可以使得控制块和对象可以分配在同一块连续的内存上,减少了内存碎片化的风险。
| 接下来模拟实现shared_ptr:
第一步:实现出RAII的框架
namespace yjz {template<classT>classshared_ptr{public:shared_ptr(T* ptr =nullptr):_ptr(ptr){}~shared_ptr(){if(_ptr)delete _ptr;}private: T* _ptr;};}第二步:如何实现引用计数
首先我们应该讨论引用计数保存在哪里,不能存在各自的对象中,应该满足一个资源配一个计数,也就是公共计数。静态成员变量计数也不行,因为静态成员变量不属于某个对象,而是属于类的所有对象,这显然是不行的,因为所有的对象不可能都指向同一资源,可能其中的几个对象分别指向不同的资源。
这里合理的处理是将引用计数开在堆上(也就是上面make_shared部分提到的控制块),然后在对象中存一个指针指向这个计数。

一个资源配一个计数,所以计数也在构造的时候给出。
namespace yjz {template<classT>classshared_ptr{public:shared_ptr(T* ptr =nullptr):_ptr(ptr),_pcount(newint(1)){}shared_ptr(const shared_ptr<T>& sp):_ptr(sp._ptr),_pcount(sp._pcount){(*_pcount)++;}~shared_ptr(){//当计数为0时释放资源if(--(*_pcount)==0){delete _ptr;delete _pcount; _ptr =nullptr; _pcount =nullptr;}} T&operator*(){return*_ptr;} T*operator->(){return _ptr;}private: T* _ptr;int* _pcount;};}第三步:赋值重载
赋值需要注意的是被赋值的指针原来管理的资源是否需要释放。那么这里就要显示地调用析构函数,不过析构函数最好和构造等一一对应,所以这里可以将析构的逻辑用一个函数重新包装,然后在赋值和析构函数中调用这个函数处理。
其中赋值还需要防止自己给自己赋值的情况,有可能会出现野指针的问题。

shared_ptr<T>&operator=(const shared_ptr<T>& sp){if(_ptr != sp._ptr){release(); _ptr = sp._ptr; _pcount = sp._pcount;++(*_pcount);}return*this;}voidrelease(){//当计数为0时释放资源if(--(*_pcount)==0){delete _ptr;delete _pcount; _ptr =nullptr; _pcount =nullptr;}}第四步:加定制删除器
在构造shared_ptr时我们要传一个可调用对象过去,这个对象可能是函数指针,可能是仿函数,也有可能是lambda,这里不确定接收的类型,就可以用function来接收。这里的删除器只供构造函数使用,因此不能传整个类模版。
//...template<classD>shared_ptr(T* ptr, D del):_ptr(ptr),_pcount(newint(1)),_del(del){}//...voidrelease(){//当计数为0时释放资源if(--(*_pcount)==0){//delete _ptr;_del(_ptr);delete _pcount; _ptr =nullptr; _pcount =nullptr;}}//...private: T* _ptr;int* _pcount; function<void(T* ptr)> _del =[](T* ptr){delete ptr;}}function成员变量需要一个缺省值,如果没有传定制的删除器需要用默认的删除操作进行释放。
完成版:
namespace test {template<classT>classshared_ptr{using delete_t = std::function<void(T* ptr)>;public:explicitshared_ptr(T* p =nullptr, delete_t del =[](T* ptr){delete ptr;}):_ptr(p),_cnt(new std::atomic<int>(1)),_del(del){}~shared_ptr(){release();}shared_ptr(const shared_ptr<T>& other):_ptr(other._ptr),_cnt(other._cnt),_del(other._del){if(_cnt)(*_cnt)++;} shared_ptr<T>&operator=(shared_ptr<T> other){swap(other);return*this;} T&operator*()const{return*_ptr;} T*operator->()const{return _ptr;}private:voidrelease(){if(_cnt &&--(*_cnt)==0){_del(_ptr);delete _cnt; _ptr = _cnt =nullptr;}}voidswap(shared_ptr<T>& tmp){ std::swap(_ptr, tmp._ptr); std::swap(_cnt, tmp._cnt); std::swap(_del, tmp._del);}private: T* _ptr; std::atomic<int>* _cnt; delete_t _del;}}第五步:解决循环引用的问题
在特殊场景,比如双向循环链表中,如果两个节点互相指向,就会出现循环引用的问题,最后导致内存泄漏。
structListNode{int _data; shared_ptr<ListNode> _next; shared_ptr<ListNode> _prev;};intmain(){ std ::shared_ptr<ListNode>n1(new ListNode); std::shared_ptr<ListNode>n2(new ListNode); n1->_next = n2; n2->_prev = n1;return0;}
这里内存泄漏的关键是:n2后定义的先析构,而n2指向的资源还有n1->_next管理,所以n2指向的资源这里还不会释放;接下来析构n1指向的资源,而n1指向的资源还有n2->_prev管理,所以n1指向的资源这里也还不会释放。最后n1和n2两个shared_ptr都释放了它们原本指向的资源还得不到释放,因为还有这两个资源内部的shared_ptr互相管理者。
为了处理这种情况的发生,出现了weak_ptr来配合shared_ptr解决这个问题。
六、weak_ptr
不同于上面的智能指针,weak_ptr不支持直接管理资源(RAII),而是配合解决shared_ptr循环引用导致的内存泄漏的缺陷。
在引用计数的场景下,把节点中的_prev和_next改成weak_ptr就可以了
weak_ptr用shared_ptr构造不增加引用计数。
当然这里只实现了一个简单的weak_ptr。
template<classT>classweak_ptr{public:weak_ptr(){}weak_ptr(const shared_ptr<T>& sp):_ptr(sp.get()){} weak_ptr<T>&operator=(const shared_ptr<T>& sp){ _ptr = sp.get();}private: T* _ptr =nullptr;};structListNode{int _data; weak_ptr<ListNode> _next; weak_ptr<ListNode> _prev;};再通过一个例子来更直观的理解 weak_ptr 具体是如何打破循环的。
classB;// 前置声明classA{public: shared_ptr<B> b_ptr;// A持有可以管理B对象的智能指针~A(){ std::cout <<"A析构~"<< std::endl;}}classB{public: shared_ptr<A> a_ptr;// B持有可以管理A对象的智能指针~B(){ std::cout <<"B析构~"<< std::endl;}}voidtest(){auto a = std::make_shared<A>();// A对象被构造并且引用计数为1auto b = std::make_shared<B>();// B对象被构造并且引用计数为1 a->b_ptr = b;// A对象中的智能指针b_ptr指向b管理的B对象,B对象引用计数为2 b->a_ptr = a;// B对象中的智能指针a_ptr指向a管理的A对象,A对象引用计数为2}intmain(){test();return0;}上述情况中,当智能指针对象 a 和 b 出作用域后会销毁调用析构函数,A对象和B对象的引用计数都只会-1,不会为0,所以A对象B对象不会析构,出现内存泄漏。
classB;// 前置声明classA{public: shared_ptr<B> b_ptr;// A持有可以管理B对象的智能指针~A(){ std::cout <<"A析构~"<< std::endl;}}classB{public: weak_ptr<A> a_ptr;// 弱指针,不增加A的引用计数~B(){ std::cout <<"B析构~"<< std::endl;}}voidtest(){auto a = std::make_shared<A>();// A对象被构造并且引用计数为1auto b = std::make_shared<B>();// B对象被构造并且引用计数为1 a->b_ptr = b;// A对象中的智能指针b_ptr指向b管理的B对象,B对象引用计数为2 b->a_ptr = a;// A对象的引用计数仍为1}intmain(){test();return0;}b对象先析构,B的引用计数-1变成1,然后a对象再析构,A的引用计数-1变成0,A对象析构,从而带动对象中的b_ptr对象析构,B的引用计数再-1变成0,B对象析构。
weak_ptr不能直接解引用(*b->a_ptr报错),需先调用lock()转shared_ptr。- 选择哪一方换 weak_ptr?
看 “所有权关系”:比如 “老师→学生”,学生不应拥有老师的所有权,就把学生里的shared_ptr<老师>换成weak_ptr<老师>;无明确所有权时,任意一方换即可。
本篇文章的分享就到这里了,如果您觉得在本文有所收获,还请留下您的三连支持哦~
