【C++进阶篇】智能指针

📅 发布时间:2026/7/16 13:53:49 👁️ 浏览次数:
【C++进阶篇】智能指针
自动内存回收通过析构函数自动释放资源避免忘记delete导致的内存泄漏异常安全性在异常抛出时仍能保证资源释放所有权语义明确资源归属关系减少悬垂指针风险1.1 auto_ptr特点拷贝时将被拷贝对象的资源转移给拷贝对象会导致被拷贝对象悬空问题访问会崩溃。**建议**坚决不要使用该智能指针。1.2 unique_ptr特点见名知意不支持拷贝只支持移动。 使用场景当某种特定场景不需要拷贝强烈建议使用它。代码语言javascriptAI代码解释int main() { unique_ptrint sp(new int[10]); //unique_ptrint sp1 sp; int* fp sp.get(); //对该指针进行操作 for (size_t i 0; i 10; i) { fp[i] i 1; } for (size_t i 0; i 10; i) { cout fp[i] ; } cout endl; cout sp交换前: ; cout sp _ptr: sp.get() endl; unique_ptrint sp1; sp1.swap(sp); cout sp交换后: ; cout sp _ptr: sp.get() endl; cout sp1 _ptr: sp1.get() endl; sp1.release();//将指针置空 cout 调用release()后: ; cout sp1 _ptr: sp1.get() endl; return 0; }输出结果从结果可以看出调用swap后资源的管理权转移给调用者对象自己置空。1.3 shared_ptr特点支持拷贝也支持移动。底层是使用计数方式来看看是哪个对象来释放和清理资源。使用场景当某种场景需要拷贝时推荐使用它。代码语言javascriptAI代码解释struct Date { int _year; int _month; int _day; Date(int year 1, int month 1, int day 1) :_year(year) , _month(month) , _day(day) { cout _year _year _month _month _day _day endl; } ~Date() { cout ~Date() endl; } }; int main() { shared_ptrDate sp(new Date(2025, 6, 9)); cout sp.use_count()endl; shared_ptrDate sp1 sp;//赋值 cout sp.use_count() endl;//引用计数 cout sp.get() endl;//获取原生指针 cout sp1.get() endl; cout sp.operator-() endl; return 0; }输出结果从结果可以看出创建出的新对象与拷贝对象指向的资源一致。代码语言javascriptAI代码解释sp-_year;上面语句访问是允许的。因为智能指针里面重载operator -() 同时返回的是管理对象的类的对象。 上述语句等价于shared_ptr通过重载operator-使得sp-_day的语法等价于**(sp)._day*。下面再看其它问题代码语言javascriptAI代码解释shared_ptrDate sp1(new Date[10]);程序会崩溃输出结果原因分析shared_ptr默认使用delete释放内存针对单个对象但new Date[ ]必须使用delete [ ] 释放资源。错误类型匹配会导致未定义行为。这里的T是Date实例化时而构造时Date[ ]导致析构时资源不匹配。解决办法使用自定义(推荐)说白了就是让智能指针管理资源的对象类型与构造时的对象数据类型一致。就可以解决了。代码语言javascriptAI代码解释shared_ptrDate[] sp1(new Date[10]);输出结果从结果可以看出构造时的10个Date对象都被正确清理了。 其它的方法可以使用仿函数对象lambda表达式函数指针对象等构造时将删除器传递给智能指针整个类删除器在类内部初始化了因为这个类内部包含指向对象指针然后释放和清理资源。代码语言javascriptAI代码解释// lambda表达式做删除器 auto delArrOBJ [](Date* ptr) {delete[] ptr; }; unique_ptrDate, decltype(delArrOBJ) up4(new Date[5], delArrOBJ); shared_ptrDate sp4(new Date[5], delArrOBJ); // 函数指针做删除器 templateclass T void DeleteArrayFunc(T* ptr) { delete[] ptr; } unique_ptrDate, void(*)(Date*) up3(new Date[5], DeleteArrayFuncDate); shared_ptrDate sp3(new Date[5], DeleteArrayFuncDate);1.4 make_sharedstd::make_shared 是 C11 引入的工厂函数用于高效、安全地创建 shared_ptr 智能指针。 基本用法代码语言javascriptAI代码解释#include memory // 创建 shared_ptrint auto sp1 std::make_sharedint(42); // 创建 shared_ptrDate class Date { /* ... */ }; auto sp2 std::make_sharedDate(2025, 6, 9);性能优化一次内存分配 传统方式shared_ptr sp(new Date(2025,6,9)); // 两次内存分配第一次分配为 Date 对象分配内存第二次分配为引用计数控制块分配内存 make_shared 方式auto sp std::make_shared(2025,6,9); // 一次内存分配底层原理内存分配策略调用 ::operator new(sizeof(T) sizeof(ControlBlock))将对象和控制块放置在同一块连续内存中二. 原理auto_ptr转移资源思路不被认可而unique_ptr不支持拷贝思路较简单下面重点看看shared_ptr设计思路及原理。思路主要这⾥⼀份资源就需要⼀个引⽤计数所以引⽤计数才⽤静态成员的⽅式是⽆法实现的要使⽤堆上动态开辟的⽅式构造智能指针对象时来⼀份资源就要new⼀个引⽤计数出来。多个shared_ptr指向资源时就引⽤计数shared_ptr对象析构时就–引⽤计数引⽤计数减到0时代表当前析构的shared_ptr是最后⼀个管理资源的对象则析构资源。问题为啥shared_ptr计数需要再堆上开空间静态方式行不行不行因为需要特定的实例对象指向资源时计数器才1因为需要一个资源共享一个计数器。 假如使用静态方式代码语言javascriptAI代码解释static int static_counter 0; // 错误所有对象共享同一个计数器导致虽然是类对象实例但未指向该资源导致计数器逻辑错误。 模拟实现shrared_ptr智能指针代码语言javascriptAI代码解释namespace W { templateclass T class shared_ptr { public: explicit shared_ptr(T* ptr nullptr) :_ptr(ptr) , _pcount(new int(1)) {} templateclass D explicit shared_ptr(T* ptr nullptr,D del) :_ptr(ptr) , _pcount(new int(1)) ,_del(del) {} shared_ptr(const shared_ptrT sp) :_ptr(sp._ptr) , _pcount(sp._pcount) ,_del(sp._del) { (*_pcount); } void release() { if (--(*_pcount) 0) { //delete _ptr; _del(_ptr); delete _pcount; _ptr nullptr; _pcount nullptr; } } shared_ptrT operator(const shared_ptrT sp) { if (this ! sp) { release(); _ptr sp._ptr; _pcout sp._pcount; (*_pcount); } return *this; } ~shared_ptr() { release(); } T operator*() { return *_ptr; } T* operator-() { return _ptr; } T* get()const { return _ptr; } int use_count()const { return *_pcount; } operator bool() { return _ptr ! nullptr; } private: T* _ptr; int* _pcount; std::functionvoid(T*) _del [](T* ptr) {delete ptr; };//默认删除器 }; }三. shared_ptr循环引用问题循环引用导致资源未被释放从而导致内存泄漏使用weak_ptr可以解决该问题减少引用个数。下面以一个场景来看看循环引用导致内存泄漏场景当n1和n2析构时计数器分别减1节点中的指针分别指向对方导致每个资源引用计数器增加1。 n1节点中的next指针指向n2n2节点中的prev指针指向n1next什么时候析构呢等着n2的prev指针不再指向是就析构了n2的prev指针什么时候析构呢等着n1的next不在指向时就析构了。又回到原始问题这就导致内存泄漏。 使用 weak_ptr 可以解决问题。代码语言javascriptAI代码解释std::weak_ptrListNode _next; std::weak_ptrListNode _prev;下面详细介绍一下weak_ptr原理功能weak_ptr是C11引入的智能指针主要用于解决shared_ptr的循环引用问题并提供对共享资源的非拥有式观察。非拥有式观察weak_ptr不增加对象的引用计数仅观察由shared_ptr管理的资源。它通过共享控制块Control Block跟踪对象状态但不会影响对象生命周期。解决循环引用当两个对象通过shared_ptr互相引用时引用计数无法归零导致内存泄漏。将其中一个引用改为weak_ptr可打破循环。例如代码语言javascriptAI代码解释class B; class A { public: std::shared_ptrB b_ptr; }; class B { public: std::weak_ptrA a_ptr; // 使用weak_ptr打破循环 };expired()快速检查对象是否存活无需创建shared_ptr。总结weak_ptr通过非拥有式观察机制有效解决了shared_ptr的循环引用问题并支持缓存、观察者模式等场景。理解其控制块共享、引用计数管理及安全访问方法能帮助开发者编写更健壮的C代码。三. 线程安全问题如果多个线程在堆上同时进行对该计数器进行操作就会导致线程安全问题。 解决办法加互斥锁。将计数器不设置为int* 类型而设置为atomic*。代码语言javascriptAI代码解释atmoicint* _pcount;四. 内存泄漏4.1 什么是内存泄漏内存泄漏指因为疏忽或错误造成程序未能释放已经不再使⽤的内存⼀般是忘记释 放或者发⽣异常释放程序未能执⾏导致的。内存泄漏并不是指内存在物理上的消失⽽是应⽤程序分 配某段内存后因为设计错误失去了对该段内存的控制因⽽造成了内存的浪费。4.2 危害普通程序运⾏⼀会就结束了出现内存泄漏问题也不⼤进程正常结束⻚表的映射 关系解除物理内存也可以释放。⻓期运⾏的程序出现内存泄漏影响很⼤如操作系统、后台服 务、⻓时间运⾏的客⼾端等等不断出现内存泄漏会导致可⽤内存不断变少各种功能响应越来越 慢最终卡死。4.3 避免内存泄漏⼯程前期良好的设计规范养成良好的编码规范申请的内存空间记着匹配的去释放。ps这个理 想状态。但是如果碰上异常时就算注意释放了还是可能会出问题。需要下⼀条智能指针来管理 才有保证。尽量使⽤智能指针来管理资源如果⾃⼰场景⽐较特殊采⽤RAII思想⾃⼰造个轮⼦管理。定期使⽤内存泄漏⼯具检测尤其是每次项⽬快上线前不过有些⼯具不够靠谱或者是收费。总结⼀下内存泄漏⾮常常⻅解决⽅案分为两种1、事前预防型。如智能指针等。2、事后查错型。如泄漏检测⼯具。