【C++从小白到大牛】C++智能指针的使用、原理和分类

发布时间:2026/7/14 13:43:52
【C++从小白到大牛】C++智能指针的使用、原理和分类 目录1、我们为什么需要智能指针2、内存泄露2.1 什么是内存泄漏内存泄漏的危害2.2如何避免内存泄漏总结一下:3.智能指针的使用及原理3.1 RAII3.2关于深拷贝和浅拷贝更深层次的理解3.3 std::auto_ptr3.4 std::unique_ptr3.5 std::shared_ptr引用计数的原理原码std::shared_ptr的循环引用特定场景下的缺陷很坑的赋值重载函数智能指针的缺陷循环引用解决方案1、我们为什么需要智能指针下面我们先分析一下下面这段程序有没有什么内存方面的问题解析这里如果是p1的new抛异常了 那么首先p1是申请空间失败的 然后程序跳转到异常处理机制如果main函数中没有对异常捕捉那么程序就终止了。如果是p2的new抛异常了 那么大体和p1抛异常一样。区别在于p1这个指针申请的空间就会内存泄漏因为p1已经申请成功了但是由于抛异常下面的delete语句不再执行div抛异常也一样 只不过这个时候p1和p2开辟的空间都会造成内存泄漏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如何避免内存泄漏1. 工程前期良好的设计规范养成良好的编码规范申请的内存空间记着匹配的去释放。ps这个理想状态。但是如果碰上异常时就算注意释放了还是可能会出问题。需要下一条智能指针来管理才有保证。2. 采用RAII思想或者智能指针来管理资源。3. 有些公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。4. 出问题了使用内存泄漏工具检测。ps不过很多工具都不够靠谱或者收费昂贵。总结一下:内存泄漏非常常见解决方案分为两种1、事前预防型。如智能指针等。2、事后查错型。如泄漏检测工具。3.智能指针的使用及原理3.1 RAIIRAIIResource Acquisition Is Initialization资源立即初始化是一种利用对象生命周期来控制程序资源如内存、文件句柄、网络连接、互斥量等等的简单技术。在对象构造时获取资源接着控制对资源的访问使之在对象的生命周期内始终保持有效最后在对象析构的时候释放资源。借此我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处不需要显式地释放资源。采用这种方式对象所需的资源在其生命期内始终保持有效。所以我们把资源的获取和释放交给了一个智能指针类让这个类帮我们完成所以就不需要关心会报错异常导致内存泄漏的问题。并且这个类还得需要将* 、-重载下才可让其像指针一样去使用。原码templateclass 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; };总结一下智能指针的原理RAII特性重载operator*和opertaor-具有像指针一样的行为。3.2关于深拷贝和浅拷贝更深层次的理解vector/list……采用深拷贝的原因利用资源存储管理数据资源是自己的拷贝时每个对象各自一份资源各管各的所以深拷贝。智能指针/迭代器……采用浅拷贝的原因本质资源不是自己的代为持有方便访问修改数据。他们拷贝的时候期望指向同一个资源所以浅拷贝3.3 std::auto_ptrC98版本的库中就提供了auto_ptr的智能指针。下面演示的auto_ptr的使用及问题。templateclass T class auto_ptr { public: // RAII auto_ptr(T* ptr) :_ptr(ptr) {} // ap2(ap1) auto_ptr(auto_ptrT ap) { _ptr ap._ptr; ap._ptr nullptr; } ~auto_ptr() { cout delete: _ptr endl; delete _ptr; } // 像指针一样 T operator*() { return *_ptr; } T* operator-() { return _ptr; } private: T* _ptr; };auto_ptr管理权转移被拷贝对象把资源管理权转移给拷贝对象导致被拷贝对象悬空注意拷贝过后不能访问被拷贝对象否则就出现空指针了。很多公司禁止使用它因为他很坑3.4 std::unique_ptrC11中开始提供更靠谱的unique_ptrunique_ptr的实现原理简单粗暴的防拷贝禁止拷贝简单粗暴适合于不需要拷贝的场景。下面简化模拟实现了一份UniquePtr来了解它的原理templateclass T class unique_ptr { public: // RAII unique_ptr(T* ptr) :_ptr(ptr) {} // ap2(ap1) //直接将拷贝函数和复制重载函数不实现赋值为delete unique_ptr(const unique_ptrT ap) delete; unique_ptrT operator(const unique_ptrT ap) delete; ~unique_ptr() { cout delete: _ptr endl; delete _ptr; } // 像指针一样 T operator*() { return *_ptr; } T* operator-() { return _ptr; } private: T* _ptr; };3.5 std::shared_ptrC11中开始提供更靠谱的并且支持拷贝的shared_ptr那我们如何才能支持拷贝呢我们采取使用引用计数的方式来解决。记录有几个对象参与管理这个资源。可以直接采用count整形直接吗不可以我们这里要求每份资源分配一个计数而这里是每指向一份资源调用的智能指针就分配一个计数也就是我们要求的是公共计数而不是每个智能指针单独的计数。可以采用静态成员变量来解决这个问题吗sharedptr 为什么用静态的引用计数不行因为静态成员属于这个类属于这个类的所有对象不可以。因为静态成员属于这个类属于这个类的所有对象。而我们的需求是每个资源配一个引用计数而不是全部是一个引用计数。所以这里只释放了一份资源另一份资源没有释放正确的解法是每个对象存一个指向计数的指针而指针指向的内容只在构造的时候进行初始化构造单独这一份拷贝的时候计数智能指针析构的时候--计数只有当引用计数减到0时才真正释放这份资源引用计数的原理1. shared_ptr在其内部给每个资源都维护了着一份计数用来记录该份资源被几个对象共享。2. 在对象被销毁时(也就是析构函数调用)就说明自己不使用该资源了对象的引用计数减一。3. 如果引用计数是0就说明自己是最后一个使用该资源的对象必须释放该资源4. 如果不是0就说明除了自己还有其他对象在使用该份资源不能释放该资源否则其他对象就成野指针了。注意这里的引用计数需要是一个指针原码templateclass T class shared_ptr { public: // RAII shared_ptr(T* ptr nullptr) :_ptr(ptr) ,_pcount(new int(1)) {} // sp2(sp1) shared_ptr(const shared_ptrT sp) { _ptr sp._ptr; _pcount sp._pcount; // 拷贝时计数 (*_pcount); } // sp1 sp4 // sp4 sp4; // sp1 sp2; shared_ptrT operator(const shared_ptrT sp) { //if (this ! sp) if (_ptr ! sp._ptr) { release(); _ptr sp._ptr; _pcount sp._pcount; // 拷贝时计数 (*_pcount); } return *this; } void release() { // 说明最后一个管理对象析构了可以释放资源了 if (--(*_pcount) 0) { cout delete: _ptr endl; delete _ptr; delete _pcount; } } ~shared_ptr() { // 析构时--计数计数减到0 release(); } int use_count() { return *_pcount; } // 像指针一样 T operator*() { return *_ptr; } T* operator-() { return _ptr; } T* get() const { return _ptr; } private: T* _ptr; int* _pcount; };std::shared_ptr的循环引用特定场景下的缺陷那我们的share_ptr智能指针就是完美的吗其实也并不是。很坑的赋值重载函数这里的sp1 sp4会将sp1的指针指向sp4指向的资源但是原来sp1指向的资源需要--引用计数不然会引起内存泄露赋值重载函数很坑注意先--计数因为赋值给别人了自己可能赋值给自己很坑shared_ptrT operator(const shared_ptrT sp) { //if (this ! sp)防止自己给自己赋值 if (_ptr ! sp._ptr) { release(); _ptr sp._ptr; _pcount sp._pcount; // 拷贝时计数 (*_pcount); } return *this; }智能指针的缺陷循环引用这里为什么n2的生命周期先到因为对象调用构造函数和调用析构函数的顺序相反这里先构造n1后构造n2,所以就会先析构n2因此n2的生命周期先到我们只有一个语句时就不会报错具体分析如下首先出了作用域n2的生命周期先到那么n2的引用计数就--变成了1再紧接着n1的生命周期到了所以n1的引用计数就-到了0因此就要释放掉就会将n1结点的资源释放掉这样n2的引用计数就会变成0所以就会将n2结点的资源也释放掉因此释放空间没问题不会造成泄露但是这两句话同时存在就会存在循环引用的问题具体分析如下1. node1和node2两个智能指针对象指向两个节点引用计数变成1我们不需要手动delete。2. node1的_next指向node2node2的_prev指向node1引用计数变成2。3. node1和node2析构引用计数减到1但是_next还指向下一个节点。但是_prev还指向上一个节点。4. 也就是说node1的_next析构了node2就释放了。5. 也就是说node2的_prev析构了node1就释放了。6. 但是_next属于node1的成员node1释放了_next才会析构而node1由node2的_prev管理_prev属于node2成员所以这就叫循环引用谁也不会释放互相牵制也就是引用计数一直在1不会减到0也就不会析构了。解决方案在循环引用的一方使用weak_ptr来打破循环引用在引用计数的场景下把节点中的_prev和_next改成weak_ptr就可以了原理就是node1-_next node2;和node2-_prev node1;时weak_ptr的_next和_prev不会增加node1和node2的引用计数。