c++智能指针

c++智能指针 为什么需要智能指针下面我们先分析一下下面这段程序有没有什么内存方面的问题提示一下注意分析MergeSort 函数中的问题。int div() { int a, b; cin a b; if (b 0) throw invalid_argument(除0错误); return a / b; } void Func() { // 1、如果p1这里new 抛异常会如何 // 2、如果p2这里new 抛异常会如何 // 3、如果div调用这里又会抛异常会如何 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; }这段代码暴露了手动管理资源在异常情况下极容易导致内存泄漏的问题这正是我们需要智能指针的根本原因。我们来逐一分析Func函数中三个可能抛异常的场景void Func() { int* p1 new int; // 场景1 int* p2 new int; // 场景2 cout div() endl;// 场景3 delete p1; delete p2; }场景 1p1这里new抛异常new int在内存不足时会抛出std::bad_alloc异常。后果p1尚未成功分配函数直接退出。此时p2还没分配没有泄漏程序跳转到main的catch正常处理。场景 2p2这里new抛异常p1已经成功分配p2分配时抛出异常。后果异常抛出后Func的剩余代码delete p1; delete p2;被跳过。p1指向的内存永远不会被释放发生内存泄漏。场景 3div()内部抛异常除零p1、p2都分配成功div()因b 0抛出异常。后果异常直接跳出Func同样跳过了后面的delete。两个指针的内存全部泄漏。核心问题在抛异常的路径上没有任何机制能保证delete被调用。手动写try-catch来兜底虽然可以但代码会变得臃肿且极易遗漏。智能指针如何解决智能指针如std::unique_ptr利用RAII资源获取即初始化原则把资源的生命周期绑定到栈对象的生命周期上void FuncSafe() { std::unique_ptrint p1(new int); std::unique_ptrint p2(new int); cout div() endl; // 无论是否抛异常离开作用域时 p1、p2 一定会被析构内存自动释放 }当异常发生时栈展开stack unwinding会销毁p1、p2这两个局部对象它们的析构函数会自动执行delete从而彻底杜绝内存泄漏实现异常安全。这也是“为什么需要智能指针”的标准答案。内存泄漏什么是内存泄漏内存泄漏的危害什么是内存泄漏内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内 存泄漏并不是指内存在物理上的消失而是应用程序分配某段内存后因为设计错误失去了对 该段内存的控制因而造成了内存的浪费。内存泄漏的危害长期运行的程序出现内存泄漏影响很大如操作系统、后台服务等等出现 内存泄漏会导致响应越来越慢最终卡死。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; }内存泄漏分类C/C程序中一般我们关心两种方面的内存泄漏堆内存泄漏(Heap leak)堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一 块内存用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分 内存没有被释放那么以后这部分空间将无法再被使用就会产生Heap Leak。系统资源泄漏指程序使用系统分配的资源比方套接字、文件描述符、管道等没有使用对应的函数释放 掉导致系统资源的浪费严重可导致系统效能减少系统执行不稳定。如何检测内存泄漏了解在linux下内存泄漏检测linux下几款内存泄漏检测工具Linux下几款C程序中的内存泄露检查工具_c内存泄露工具分析-CSDN博客在windows下使用第三方工具VLD工具说明VS编程内存泄漏VLD(Visual LeakDetector)内存泄露库_visual leak detector vs2020-CSDN博客其他工具内存泄漏工具比较内存泄露检测工具比较 - 默默淡然 - 博客园如何避免内存泄漏1. 工程前期良好的设计规范养成良好的编码规范申请的内存空间记着匹配的去释放。ps 这个理想状态。但是如果碰上异常时就算注意释放了还是可能会出问题。需要下一条智 能指针来管理才有保证。2. 采用RAII思想或者智能指针来管理资源。3. 有些公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项4. 出问题了使用内存泄漏工具检测。ps不过很多工具都不够靠谱或者收费昂贵。总结一下: 内存泄漏非常常见解决方案分为两种1、事前预防型。如智能指针等。2、事后查错型。如泄 漏检测工具。智能指针的使用及原理RAIIRAIIResource Acquisition Is Initialization是一种利用对象生命周期来控制程序资源如内 存、文件句柄、网络连接、互斥量等等的简单技术。在对象构造时获取资源接着控制对资源的访问使之在对象的生命周期内始终保持有效最后在对象析构的时候释放资源。借此我们实际上把管理一份资源的责任托管给了一个对象。这种做 法有两大好处不需要显式地释放资源。采用这种方式对象所需的资源在其生命期内始终保持有效。下面看一段代码int div() { int a, b; cin a b; if (b 0) throw invalid_argument(除0错误); return a / b; } void func() { int* ptr new int; //... cout div() endl; //... delete ptr; } int main() { try { func(); } catch (exception e) { cout e.what() endl; } return 0; }执行上述代码时如果用户输入的除数为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* ptr new int; try { cout div() endl; } catch (...) { delete ptr; throw; } delete ptr; } int main() { try { func(); } catch (exception e) { cout e.what() endl; } return 0; }上述问题也可以使用智能指针进行解决。比如templateclass 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() { SmartPtrint sp(new int); //... cout div() endl; //... } int main() { try { func(); } catch (exception e) { cout e.what() endl; } return 0; }代码中将申请到的内存空间交给了一个SmartPtr对象进行管理。在构造SmartPtr对象时SmartPtr将传入的需要被管理的内存空间保存起来。在SmartPtr对象析构时SmartPtr的析构函数中会自动将管理的内存空间进行释放。此外为了让SmartPtr对象能够像原生指针一样使用还需要对*和-运算符进行重载。所以无论程序是正常执行完毕返回了还是因为某些原因中途返回了或是因为抛异常返回了只要SmartPtr对象的生命周期结束就会调用其对应的析构函数进而完成内存资源的释放。智能指针的原理实现智能指针时需要考虑以下三个方面的问题1、在对象构造时获取资源在对象析构的时候释放资源利用对象的生命周期来控制程序资源即RAII特性。2、对*和-运算符进行重载使得该对象具有像指针一样的行为。3、智能指针对象的拷贝问题。概念说明 RAIIResource Acquisition Is Initialization是一种利用对象生命周期来控制程序资源如内存、文件句柄、互斥量等等的简单技术。为什么要解决智能指针对象的拷贝问题对于当前实现的SmartPtr类如果用一个SmartPtr对象来拷贝构造另一个SmartPtr对象或是将一个SmartPtr对象赋值给另一个SmartPtr对象都会导致程序崩溃。比如int main() { SmartPtrint sp1(new int); SmartPtrint sp2(sp1); //拷贝构造 SmartPtrint sp3(new int); SmartPtrint sp4(new int); sp3 sp4; //拷贝赋值 return 0; }原因1、编译器默认生成的拷贝构造函数对内置类型完成值拷贝浅拷贝因此用sp1拷贝构造sp2后相当于这sp1和sp2管理了同一块内存空间当sp1和sp2析构时就会导致这块空间被释放两次。2、编译器默认生成的拷贝赋值函数对内置类型也是完成值拷贝浅拷贝因此将sp4赋值给sp3后相当于sp3和sp4管理的都是原来sp3管理的空间当sp3和sp4析构时就会导致这块空间被释放两次并且还会导致sp4原来管理的空间没有得到释放。需要注意的是智能指针就是要模拟原生指针的行为当我们将一个指针赋值给另一个指针时目的就是让这两个指针指向同一块内存空间所以这里本就应该进行浅拷贝但单纯的浅拷贝又会导致空间被多次释放因此根据解决智能指针拷贝问题方式的不同从而衍生出了不同版本的智能指针。C中的智能指针std::auto_ptrauto_ptr是C98中引入的智能指针auto_ptr通过管理权转移的方式解决智能指针的拷贝问题保证一个资源在任何时刻都只有一个对象在对其进行管理这时同一个资源就不会被多次释放了。比如int main() { std::auto_ptrint ap1(new int(1)); std::auto_ptrint ap2(ap1); *ap2 10; //*ap1 20; //error std::auto_ptrint ap3(new int(1)); std::auto_ptrint ap4(new int(2)); ap3 ap4; return 0; }但一个对象的管理权转移后也就意味着该对象不能再用对原来管理的资源进行访问了否则程序就会崩溃因此使用auto_ptr之前必须先了解它的机制否则程序很容易出问题很多公司也都明确规定了禁止使用auto_ptr。auto_ptr的模拟实现1. 核心思想auto_ptr利用RAII资源获取即初始化机制让对象的生命周期与资源动态内存绑定构造时获取资源析构时释放资源。通过重载*和-模拟原生指针行为。拷贝和赋值时转移所有权而非共享资源防止重复释放。2. 设计要点所有权管理接口化引入release()、reset()、get()和swap()等标准智能指针常用接口让资源管理逻辑从拷贝构造和赋值中分离出来职责更单一代码更清晰安全。拷贝语义即所有权转移拷贝构造函数和拷贝赋值运算符的参数都不是const的因为它们需要修改传入对象将其内部指针置空。内部通过调用ap.release()取出资源并移交。自赋值安全赋值运算符中通过if (this ! ap)防止自赋值。配合reset(ap.release())即使不检查自赋值ap.release()也会在自赋值时先释放自身再置空但显式检查更直观且能避免无谓的delete。防止误删同一指针reset()中判断if (_ptr ! ptr)防止用户reset(get())时误删还持有的资源。显式构造构造函数标记为explicit避免隐式类型转换导致意外接管资源。代码#include iostream #include utility // std::swap namespace cl { templateclass T class auto_ptr { public: //构造与析构 //构造函数接管原始指针explicit 防止隐式转换 explicit auto_ptr(T* ptr nullptr) : _ptr(ptr) {} // 析构函数释放管理的资源delete nullptr 安全 ~auto_ptr() { delete _ptr; // 调试可取消注释std::cout delete: _ptr std::endl; } //拷贝所有权转移 //拷贝构造函数从 ap 接管资源ap 被置空 auto_ptr(auto_ptr ap) : _ptr(ap.release()) {} //拷贝赋值运算符释放自身接管 apap 被置空 auto_ptr operator(auto_ptr ap) { if (this ! ap) { reset(ap.release()); } return *this; } //指针行为 T operator*() const { return *_ptr; } T* operator-() const { return _ptr; } // 获取原始指针不转移所有权 T* get() const { return _ptr; } // 所有权管理 // 交出所有权返回原始指针自身置空 T* release() { T* tmp _ptr; _ptr nullptr; return tmp; } // 重置资源释放旧资源接管新指针 void reset(T* ptr nullptr) { if (_ptr ! ptr) // 防止 delete 自身已持有的指针 { delete _ptr; _ptr ptr; } } // 交换两个 auto_ptr 管理的资源 void swap(auto_ptr ap) { std::swap(_ptr, ap._ptr); } private: T* _ptr; // 管理的资源 }; } // namespace cl提醒不能管理数组析构使用delete而非delete[]因此禁止用auto_ptr管理new T[]分配的数组。不可放入标准容器它的拷贝会转移所有权破坏容器对元素“可拷贝”的预期如std::vector在扩容时会拷贝元素导致原指针被置空引发未定义行为。C11 起已被std::unique_ptr仅支持移动取代。