【C++进阶】智能指针(万字详解) 分手后的思念是犯贱 2024-04-20 06:36 90阅读 0赞 > **?C++学习历程:入门** > > -------------------- > > * **博客主页:**[一起去看日落吗][Link 1] > * **持续分享博主的C++学习历程** > * **`博主的能力有限,出现错误希望大家不吝赐教`** > * **分享给大家一句我很喜欢的话:** 也许你现在做的事情,暂时看不到成果,但不要忘记,树?成长之前也要扎根,也要在漫长的时光?中沉淀养分。静下来想一想,哪有这么多的天赋异禀,那些让你羡慕的优秀的人也都曾默默地翻山越岭?。 > > -------------------- > > ![在这里插入图片描述][eabca17da9704379a5f15832a495b4cc.jpeg_pic_center] ? ? ? ? ? -------------------- #### 目录 #### * ?1. 初识智能指针 * * ?1.1 什么是智能指针 * ?1.2 为什么需要智能指针 * ?2. 内存泄露 * * ?2.1 什么是内存泄漏 * ?2.2 内存泄漏分类(了解) * ?2.3 如何检测内存泄漏(了解) * ?2.4 如何避免内存泄漏 * ?3. 智能指针的使用及原理 * * ?3.1 RAII * ?3.2 智能指针的原理 * ?3.3 auto\_ptr * ?3.4 unique\_ptr * ?3.5 shared\_ptr * ?3.6 循环引用 * ?3.7 定制删除器 * ?3.8 weak\_ptr * ?4. 智能指针的模拟实现 ## ?1. 初识智能指针 ## ### ?1.1 什么是智能指针 ### 智能指针不是指针,是一个管理指针的类,用来存储指向动态分配对象的指针,负责自动释放动态分配的对象,防止堆内存泄漏。 动态分配的资源,交给一个类对象去管理,当类对象声明周期结束时,自动调用析构函数释放资源 -------------------- ### ?1.2 为什么需要智能指针 ### 我们注意观察这段代码 void File() { string filename; cin >> filename; FILE* fout = fopen(filename.c_str(), "r"); if (fout == nullptr) { string errmsg = "打开文件失败:"; errmsg += filename; errmsg += "->"; errmsg += strerror(errno); Exception e(errno, errmsg); throw e; } char ch; while ((ch = fgetc(fout))!=EOF) { cout << ch; } fclose(fout); } double Division(int a, int b) { if (b == 0) { string errmsg = "Division by zero condition!"; Exception e(100, errmsg); throw e; } else { return ((double)a / (double)b); } } void Func() { int* p = new int[100]; int len, time; cin >> len >> time; try { cout << Division(len,time) << endl; File(); } catch (...) { //捕获之后,不是要处理异常,异常由最外层同一处理 //这里捕获异常只是为了处理内存泄漏的问题 delete[]p; throw; } delete[]p; } int main() { try { Func(); } catch (const Exception& e) { cout << e.what() << endl; } catch (...) { cout << "未知异常" << endl; } return 0; } 在Func函数中,我们在堆上创建了开一个指针,为了防止函数抛出异常导致最后的 析构函数不执行而产生野指针,我们使用了 异常的重新抛出策略。 但是,终究不是个好的方法,如果这类资源较多,那么我们需要大量的 异常重抛 ,而且就算程序不涉及程序处理,大量的堆上空间需要人工释放,容易造成疏漏,这一问题在工程中比较常见。 所以,这时候如果我们实用智能指针,就可以不用再操心内存是否会泄露的问题 **所以智能指针的出现是为了解决内存泄漏问题的** -------------------- ## ?2. 内存泄露 ## ### ?2.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; } -------------------- ### ?2.2 内存泄漏分类(了解) ### **C/C++程序中一般我们关心两种方面的内存泄漏:** * 堆内存泄漏(Heap leak) 堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。 * 系统资源泄漏 指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。 -------------------- ### ?2.3 如何检测内存泄漏(了解) ### * 在linux下内存泄漏检测:[linux下几款内存泄漏检测工具][linux] * 在windows下使用第三方工具:[VLD工具说明][VLD] * 其他工具:[内存泄漏工具比较][Link 2] -------------------- ### ?2.4 如何避免内存泄漏 ### 1. 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。ps:这个理想状态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。需要下一条智能指针来管理才有保证。 2. 采用RAII思想或者智能指针来管理资源。 3. 有些公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。 4. 出问题了使用内存泄漏工具检测。ps:不过很多工具都不够靠谱,或者收费昂贵。 **总结一下:** 内存泄漏非常常见,解决方案分为两种: 1. 事前预防型。如智能指针等。 2. 事后查错型。如泄漏检测工具。 -------------------- ## ?3. 智能指针的使用及原理 ## ### ?3.1 RAII ### RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。 **在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。** 这种做法有两大好处: * 不需要显式地释放资源。 * 采用这种方式,对象所需的资源在其生命期内始终保持有效 我们可以借助RALL思想来写一个简单的 智能指针: #include<iostream> using namespace std; template<class T> class SmartPtr { public: SmartPtr(T* ptr =nullptr) :_ptr(ptr) { } ~SmartPtr() { if (_ptr)delete _ptr; cout<<"~SmartPtr"<<endl; } private: T* _ptr; }; int main() { int* a = new int(1); SmartPtr<int> sp(a); //将a 指针委托给sp对象管理 SmartPtr<int>sp2(new int(2)); //直接船舰匿名对象给sp2管理 } -------------------- ### ?3.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> sp1(new int); *sp1 = 10 cout<<*sp1<<endl; SmartPtr<int> sparray(new Date); // 需要注意的是这里应该是sparray.operator->()->_year = 2018; // 本来应该是sparray->->_year这里语法上为了可读性,省略了一个-> sparray->_year = 2018; sparray->_month = 1; sparray->_day = 1; } 总结一下智能指针的原理: 1. RAII特性 2. 重载operator\*和opertaor->,具有像指针一样的行为。 -------------------- ### ?3.3 auto\_ptr ### 在C++98版本的库种,提供了 auto\_ptr 的智能指针: [auto\_ptr文档介绍][auto_ptr] 我们使用一下std::auto\_ptr: template<class T> class auto_ptr { public: auto_ptr(T* p) :_ptr(p) { } auto_ptr(auto_ptr<T>& ap) : _ptr(ap._ptr) { ap._ptr = nullptr; } ~auto_ptr() { if (_ptr) { delete _ptr; } } private: T* _ptr; }; void test_auto_ptr() { auto_ptr<int> ap1(new int); auto_ptr<int> ap2(ap1); auto_ptr<int> ap3(ap2); } **auto\_ptr的实现原理:管理权转移的思想,拷贝时会导致原对象悬空** 一般的公司都有明确的条文规定不可以使用auto\_ptr,因为这真的是一个很不负责任的设计 ![在这里插入图片描述][be7b52da2522413e89505b51b22bc4b5.png] -------------------- ### ?3.4 unique\_ptr ### [unique\_ptr的文档][unique_ptr] unique\_ptr相比于auto\_ptr的解决方法就是:简单粗暴,防拷贝 template<class T> class unique_ptr { public: unique_ptr(T* p) :_ptr(p) { } //C++98里面直接把拷贝构造定义为私有且只声明不定义 //C++11就是直接调用delete不让你生成 unique_ptr(const unique_ptr<T>& ap) = delete; unique_ptr<T>& operator=(const unique_ptr<T>& ap) = delete; T* operator->() { return _ptr; } T& operator*() { return *_ptr; } T* get() { return _ptr; } ~unique_ptr() { if (_ptr) { delete _ptr; } } private: T* _ptr; }; //如果非要写一个只能指针写unique_ptr是最简单的,一定要抓住智能指针的两大特性 void test_unique_ptr() { unique_ptr<int> up1(new int(1)); cout << *up1 << endl; *up1 = 10; cout << *up1 << endl; //unique_ptr<int> up2(up1); //这也就做到了防拷贝 } -------------------- ### ?3.5 shared\_ptr ### 但也总不能不拷贝呀,所以unique\_ptr也不会一个好的方式呀,所以引入了shared\_ptr **shared\_ptr 是当前最为广泛使用的智能指针,它可以安全的提供拷贝操作。** [shared\_ptr的文档介绍][shared_ptr] 那么shared\_ptr的原理是什么? 我们可以对一个资源添加一个计数器,让所有管理该资源的智能共用这个计数器,倘若发生拷贝,计数器加一,倘若有析构发生, 计数器减一,当计数器等于0的时候,就把对象析构掉。 再具体一点: 1. shared\_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。 2. 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。 3. 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源; 4. 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。 ![在这里插入图片描述][46b01adcdda04317b8ef4ede45812552.png] //shared_ptr的模拟实现 template<class T> class shared_ptr { public: shared_ptr(T* ptr = nullptr) :_ptr(ptr) ,_pRefCount(new int(1)) ,_pmtx(new mutex) { } shared_ptr(const shared_ptr<T>& sp) :_ptr(sp._ptr) ,_pRefCount(sp._pRefCount) ,_pmtx(sp._pmtx) { AddRef(); } void Release() { _pmtx->lock(); bool flag = false; if(--(*_pRefCount) == 0 && _ptr) { delete _ptr; delete _pRefCount; flag = true; } _pmtx->unlock(); if(flag == true) { delete _pmtx; } } void AddRef() { _pmtx->lock(); ++(*_pRefCount); _pmtx->unlock(); } shared_ptr<T>& operator=(const shared_ptr<T>& sp) { if(_ptr != sp._ptr) { Release(); _ptr = sp._ptr; _pRefCount = sp._pRefCount; _pmtx = sp._pmtx; AddRef(); } return *this; } int use_count() { return *_pRefCount; } ~shared_ptr() { Release(); } //像指针一样使用 T& operator*() { return *_ptr; } T* operator->() { return _ptr; } private: T* _ptr; int* _pRefCount; mutex* _pmtx; }; **线程安全问题** 1. 智能指针对象中引用计数是多个智能指针对象共享的,两个线程中智能指针的引用计数同时++或–,这个操作不是原子的,引用计数原来是1,++了两次,可能还是2.这样引用计数就错乱了。会导致资源未释放或者程序崩溃的问题。所以只能指针中引用计数++、–是需要加锁(但是引用计数是在堆上开辟的资源,所以为了能够确保使用同一把锁,锁资源也应该在堆上进行开辟)的,也就是说引用计数的操作是线程安全的。 2. 智能指针管理的对象存放在堆上,两个线程中同时去访问,会导致线程安全问题。 -------------------- ### ?3.6 循环引用 ### struct ListNode { int _data; shared_ptr<ListNode> _prev; shared_ptr<ListNode> _next; ~ListNode() { cout << "~ListNode()" << endl; } }; int test_cycle_ref() { 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; return 0; } 对于上述这段代码程序是崩溃的。 循环引用分析: 1. node1和node2两个智能指针对象指向两个节点,引用计数变成1,我们不需要手动delete。 2. node1的\_next指向node2,node2的\_prev指向node1,引用计数变成2。 3. node1和node2析构,引用计数减到1,但是\_next还指向下一个节点。但是\_prev还指向上一个节点。 4. 也就是说\_next析构了,node2就释放了。 5. 也就是说\_prev析构了,node1就释放了。 6. 但是\_next属于node的成员,node1释放了,\_next才会析构,而node1由\_prev管理,\_prev属于node2成员,所以这就叫循环引用,谁也不会释放 ![在这里插入图片描述][b1e0e644f9664805af21c48f9175fb2e.png]**解决方案**:在引用计数的场景下,把节点中的\_prev和\_next改成weak\_ptr就可以了 **原理**:node1->\_next = node2;和node2->\_prev = node1;时weak\_ptr的\_next和\_prev不会增加node1和node2的引用计数。 -------------------- ### ?3.7 定制删除器 ### 不管是我们自己实现的shared\_ptr还是库中的shared\_ptr,我们在析构的时候默认都是 delete \_ptr,如果我们托管的类型是 new T\[\] ,或者 malloc出来的话,就导致类型不是匹配的,无法析构。 为此,shared\_ptr提供了 定制删除器,我们可以在构造的时候作为参数传入。如果我们不传参,就默认使用delete ![在这里插入图片描述][709a97092e334460add4a88dd187c08f.png] 举个例子 template<class T> struct DeleteArray { void operator()(T* ptr) { delete[]ptr; } }; void test_deletor() { DeleteArray<string>da; //使用仿函数定制 std::shared_ptr<string>s2(new string[10], da); std::shared_ptr<string>s3((string*)malloc(sizeof(string)), [](string* ptr) { free(ptr); }); //使用lamdba 定制 } 如果我们也想自己是想一下呢? 当然是可以的,但是由于我们的实现比库中的简单很多(库中使用多个类),所以我们难以通过传参的方式来定制删除器,我们增加一个模板参数,通过向模板传参来达到相同的目的。 std的框架设计底层用一个类专门管理资源计数,所以它们可以在构造函数传参,把删除器类型传递给专门管理资源的这个类。而我们是一体化的。 template<class T> struct DefaultDel { void operator ()(T* ptr) { delete ptr; } }; template<class T,class D=DefaultDel<T>> //增加模板参数 class shared_ptr { public: explicit shared_ptr(T* ptr = nullptr) :_ptr(ptr), _pcount(new int(1)), _pmtx(new mutex) { } void add_ref() { _pmtx->lock(); ++(*_pcount); _pmtx->unlock(); } void release_ref() { bool flag = false; _pmtx->lock(); if (--(*_pcount) == 0 && _ptr) { //定制化删除 D del; del(_ptr); delete _pcount; flag = true; cout << "释放资源:" << _ptr << endl; } _pmtx->unlock(); if (flag)delete _pmtx; } shared_ptr(const shared_ptr<T,D>& sp) :_ptr(sp._ptr),_pcount(sp._pcount),_pmtx(sp._pmtx) { add_ref(); } shared_ptr<T,D>& operator = (const shared_ptr<T,D>& sp) { if (_ptr != sp._ptr) { if (--(*_pcount) == 0){ delete _pcount; delete _ptr; } _ptr = sp._ptr; _pcount = sp._pcount; add_ref(); } return *this; } T& operator *() { return *_ptr; } T* operator ->() { return _ptr; } T* get() { return _ptr; } int use_count() { return *_pcount; } ~shared_ptr() { release_ref(); } private: T* _ptr; int* _pcount; mutex* _pmtx; }; ### ?3.8 weak\_ptr ### **跟其他智能指针有本质区别:不支持RAII,不释放资源;专门为解决shared\_ptr循环引用而存在,不增加引用计数。** 那么shared\_ptr那么好,是不是只要使用了就不在出现内存泄漏的问题了呢? 答案是:不,因为使用shared\_ptr有可能会导致一种循环引用的问题出现。而weak\_ptr就是专门为了解决shared\_ptr中的循环引用问题的。 template<class T> class weak_ptr { public: weak_ptr() :_ptr(nullptr) { } //保留shared_ptr的指针 weak_ptr(const shared_ptr<T>& sp) :_ptr(sp.get()) { } weak_ptr& operator=(shared_ptr<T>& sp) { _ptr = sp.get(); return *this; } T& operator*() { return *_ptr; } T* operator->() { return _ptr } private: T* _ptr; }; struct ListNode { int val; //ListNode* next; //ListNode* prev; //shared_ptr<ListNode> next; //shared_ptr<ListNode> prev; weak_ptr<ListNode> next; weak_ptr<ListNode> prev; ~ListNode() { cout << "~ListNode()" << endl; } }; void test_cycle_ref() { //ListNode* p = new ListNode; //delete p; 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; } ![在这里插入图片描述][ec1fe629b7454f3ebd2f4c7c3ed2da78.png] -------------------- ## ?4. 智能指针的模拟实现 ## // // main.cpp // 智能指针 // // Created by 卜绎皓 on 2022/12/23. // #include<iostream> #include<memory> #include<mutex> using namespace std; namespace byih { //auto_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 = nullptr; } return *this; } ~auto_ptr() { if(_ptr) { delete _ptr; } } //像指针一样使用 T& operator*() { return *_ptr; } T* operator->() { return _ptr; } private: T* _ptr; }; //unique_ptr的模拟实现 template<class T> class unique_ptr { public: unique_ptr(T* ptr) :_ptr(ptr) { } ~unique_ptr() { if(_ptr) 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; }; //shared_ptr的模拟实现 template<class T> class shared_ptr { public: shared_ptr(T* ptr = nullptr) :_ptr(ptr) ,_pRefCount(new int(1)) ,_pmtx(new mutex) { } shared_ptr(const shared_ptr<T>& sp) :_ptr(sp._ptr) ,_pRefCount(sp._pRefCount) ,_pmtx(sp._pmtx) { AddRef(); } void Release() { _pmtx->lock(); bool flag = false; if(--(*_pRefCount) == 0 && _ptr) { delete _ptr; delete _pRefCount; flag = true; } _pmtx->unlock(); if(flag == true) { delete _pmtx; } } void AddRef() { _pmtx->lock(); ++(*_pRefCount); _pmtx->unlock(); } shared_ptr<T>& operator=(const shared_ptr<T>& sp) { if(_ptr != sp._ptr) { Release(); _ptr = sp._ptr; _pRefCount = sp._pRefCount; _pmtx = sp._pmtx; AddRef(); } return *this; } int use_count() { return *_pRefCount; } ~shared_ptr() { Release(); } //像指针一样使用 T& operator*() { return *_ptr; } T* operator->() { return _ptr; } private: T* _ptr; int* _pRefCount; mutex* _pmtx; }; //简化版本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; }; } -------------------- [Link 1]: https://blog.csdn.net/m0_60338933?type=blog [eabca17da9704379a5f15832a495b4cc.jpeg_pic_center]: https://image.dandelioncloud.cn/pgy_files/images/2024/04/20/73947264ca554250b334f69dcfd287e8.jpeg [linux]: https://blog.csdn.net/gatieme/article/details/51959654 [VLD]: https://blog.csdn.net/GZrhaunt/article/details/56839765 [Link 2]: https://www.cnblogs.com/liangxiaofeng/p/4318499.html [auto_ptr]: https://cplusplus.com/reference/memory/auto_ptr/ [be7b52da2522413e89505b51b22bc4b5.png]: https://image.dandelioncloud.cn/pgy_files/images/2024/04/20/dd836c62d55e4c52b669ad761dbc7a3c.png [unique_ptr]: https://cplusplus.com/reference/memory/unique_ptr/ [shared_ptr]: https://cplusplus.com/reference/memory/shared_ptr/ [46b01adcdda04317b8ef4ede45812552.png]: https://image.dandelioncloud.cn/pgy_files/images/2024/04/20/a08b429280b94e35bf7fbb9d021c63a4.png [b1e0e644f9664805af21c48f9175fb2e.png]: https://image.dandelioncloud.cn/pgy_files/images/2024/04/20/18d43675165242f9b18d15998f7d2a04.png [709a97092e334460add4a88dd187c08f.png]: https://image.dandelioncloud.cn/pgy_files/images/2024/04/20/950881470a414ea7a979c42ec4b939c8.png [ec1fe629b7454f3ebd2f4c7c3ed2da78.png]: https://image.dandelioncloud.cn/pgy_files/images/2024/04/20/063932629d5b478e8868debeb84361c7.png
相关 【C++进阶】map和set( 万字详解)—— 上篇 C++map和set详解上篇,很快就会更新下篇,实现avl树红黑树和封装map和set 旧城等待,/ 2024年04月20日 06:24/ 0 赞/ 69 阅读
相关 【C++进阶】:智能指针 智能指针 一.为什么需要智能指针 二.智能指针的原理和使用 1.RALL 2.auto\_ptr 3.unique\_ 迷南。/ 2024年02月24日 00:41/ 0 赞/ 71 阅读
相关 C++进阶STL-智能指针 [参考1][Link 1] [参考2][Link 2] 文章目录 介绍 四种智能指针 std::auto\_ptr(已 淩亂°似流年/ 2022年01月26日 23:51/ 0 赞/ 239 阅读
还没有评论,来说两句吧...