C++ 智能指针零基础笔记:从「内存又泄漏了」到「我再也不写 delete」
发布时间:2026/9/1 15:56:35 作者:尧图编辑部 阅读量:1,286

写给被new/delete折磨过的你。 这篇不讲虚的先看你一定踩过的坑再讲三种智能指针各治什么病最后给一张「什么时候用哪个」的决策表。第 0 章一句话点题智能指针就是一个「会自动帮你 delete 的指针套壳」。 你把new出来的对象交给它它死的时候出了作用域自动帮你把对象释放掉。就这么点东西。但它解决了 C 里最古老、最疼的一类事故。第 1 章痛点——裸指针的三大事故先看一段每个 C 程序员都写过的代码void DoWork() { Result* p new Result(); p-Parse(); delete p; }看起来很安全事故来了。事故一忘了 delete内存泄漏中间任何一步return、continue、抛异常delete就永远执行不到void DoWork() { Result* p new Result(); if (!p-Parse()) return; // ← 泄漏p 永远没人释放 delete p; }设备软件 7×24 小时跑每次操作漏 100 字节跑一个月就是几个 G。现场表现为「软件越跑越慢重启就好」——查这种问题的痛苦懂的都懂。事故二delete 了两次重复释放Result* p new Result(); m_List.push_back(p); delete p; // ... 后面又有人从 m_List 里拿出来 delete 了一次 → 未定义行为可能当场崩可能不崩最阴险的是可能不立刻崩等它崩的时候现场离真正的凶手已经十万八千里。事故三delete 之后还在用野指针delete p; p-Show(); // p 指向的内存已经还回去了这里在读一块「别人的地」事故的根三个事故同一个根「谁负责 delete」这件事全靠程序员脑子记。 人脑记不住的几千行代码、五六个人维护、函数传来传去谁也说不清这个指针现在归谁管。智能指针的思路就一句话别靠人记让「所有权」跟着对象走写在代码里。第 2 章智能指针的本质——RAII智能指针背后只有一个思想叫 RAII资源获取即初始化。别被名字吓到它说的是把「释放资源」绑在「对象的析构函数」上。对象出了作用域自动析构析构里自动释放。你自己 5 分钟就能写一个最简陋的智能指针templatetypename T class MyPtr { T* m_p; public: MyPtr(T* p) : m_p(p) {} ~MyPtr() { delete m_p; } // ← 灵魂在这一行 T* operator-() { return m_p; } }; void DoWork() { MyPtrResult p(new Result()); p-Parse(); if (失败) return; // 随便 return析构照样执行delete 照样调用 } // ← p 在这里析构内存自动释放看懂了这段你就懂了所有智能指针。区别只在于这个「壳」允不允许复制、能不能多人共享。 标准库给了三种壳对应三种「所有权关系」。第 3 章unique_ptr——「这东西只有我一个人的」生活类比你买的房子房产证上只有你一个名字。你要卖转移所有权得先过户过户完你就没了。规则独占同一时刻只有一个 unique_ptr 指向这个对象不许拷贝不能复印房产证可以移动可以过户过户后原指针变空std::unique_ptrResult p1(new Result()); std::unique_ptrResult p2 p1; // ❌ 编译错误不许拷贝 std::unique_ptrResult p3 std::move(p1); // ✅ 过户p1 变空更推荐的创建方式C14 起auto p std::make_uniqueResult(); // 等价于 new但更简洁安全什么时候用默认就用它。 一个对象有明确的、唯一的「主人」时函数内部 new 一个临时对象用类成员独占一个子对象手臂独占自己的运动控制卡对象工厂函数创建对象后返回给调用者std::unique_ptrRecipe LoadRecipe(const CString name) { auto p std::make_uniqueRecipe(); if (!p-Load(name)) return nullptr; // 失败返回空调用方拿到空自己判断 return p; // 返回 移动所有权交给调用者 }一句话能用 unique_ptr 的地方就不要用别的。 它开销为零和裸指针一样大、一样快还把「归我管」三个字写在了类型上。第 4 章shared_ptr——「这东西好几个人都要用」生活类比合租的房子每人一把钥匙。最后一个退房的人负责锁门还钥匙。 谁也不知道自己是不是最后一个所以门后挂了个计数牌进来一个人 1走一个人 -1减到 0 的那个人锁门。这个计数牌就是引用计数。规则auto p1 std::make_sharedResult(); // 计数 1 { auto p2 p1; // 计数 2 auto p3 p1; // 计数 3 } // p2、p3 析构计数 1 // p1 析构计数 0 → 对象在这里才被 delete对象什么时候释放最后一个持有它的 shared_ptr 死掉的时候。 你不需要知道谁是最后一个计数器替你管。什么时候用多个模块都要持有同一个对象设备软件里最典型的场景一个报警对象好几个地方都要拿着它——报警列表里存一份界面要显示SECS 上报队列里存一份消除时要再发一次给 Host日志模块存一份要写库这个报警什么时候能释放等所有地方都不用的时候。 这正是 shared_ptr 的主场std::vectorstd::shared_ptrResult m_AlarmVec; // 报警列表持有一份 void OnAlarm(std::shared_ptrResult alarm) { m_AlarmVec.push_back(alarm); // 计数 1 m_Secs-SendAlarm(alarm); // 传给 SECS 模块计数再 1 // 谁先用完谁先析构最后一个用完的负责释放 }如果这里用裸指针你就得回答一个要命的问题「这三个模块谁负责 delete」——答不上来就是泄漏或野指针。shared_ptr 把这个问题消灭了。代价每个 shared_ptr 带一个计数器内存比裸指针大通常两个指针大小计数加减是原子操作为了线程安全有一点点性能开销所以不是共享就别用 shared_ptr用 unique_ptr。第 5 章weak_ptr——「我只想看看不负责收尸」shared_ptr 的死穴循环引用struct Arm { std::shared_ptrBuffer m_buffer; }; struct Buffer { std::shared_ptrArm m_arm; // 互相持有 }; auto arm std::make_sharedArm(); auto buf std::make_sharedBuffer(); arm-m_buffer buf; // buf 计数 2 buf-m_arm arm; // arm 计数 2函数结束arm、buf两个局部变量析构计数各减 1——但都还剩 1对方手里还拿着。谁也不为 0谁也不释放。两个对象互相拽着对方一起泄漏了。就像两个人互相抓着对方的手喊「你先放」「不你先放」。weak_ptr 来破局weak_ptr 是 shared_ptr 的「跟班」它看着对象但不算计数不增加引用计数对象死了它能知道要用的时候先「升级」成 shared_ptr升级失败说明对象已经没了struct Buffer { std::weak_ptrArm m_arm; // ← 改成 weak我看着你但不拽着你 }; // 用的时候 if (auto arm m_arm.lock()) { // lock() 升级成功 对象还活着 arm-DoSomething(); } else { // 对象已经没了别用了 }生活类比shared_ptr 是钥匙有钥匙 房子不能拆weak_ptr 是访客登记簿——登记了不代表房子不能拆你来访时先问一句「房子还在吗」在就进去不在就算了。什么时候用打破循环引用互相持有的两方强势方用 shared_ptr弱势方用 weak_ptr一般是「父持有子用 shared子回指父用 weak」观察者/缓存「我想盯着这个对象但我的存在不该阻止它死亡」——界面盯着一个数据对象、缓存记着一个条目都是这个关系第 6 章到底用哪个一张决策表按顺序问自己三个问题这个对象有几个人「负责」它 │ ├─ 就我一个用完就扔 / 我是唯一主人 │ → unique_ptr默认选择零开销 │ ├─ 好几个人都要持有说不清谁最后用完 │ → shared_ptr │ └─ 我只想看看它还在不在不负责它的生死 → weak_ptr再补一条函数参数怎么传场景传法函数只是用一下不关心所有权传引用或裸指针void f(Result r)函数要拿走所有权传unique_ptr值传递内部 move函数要共享所有权传const shared_ptr或值函数只是可能用一下传weak_ptr新手最常见的浪费函数明明只是用一下还按值传shared_ptr白白加减一次计数。第 7 章新手必踩的坑1. 用裸指针构造了两个独立的 shared_ptr。Result* raw new Result(); std::shared_ptrResult p1(raw); std::shared_ptrResult p2(raw); // ☠️ 两个计数器互不知情double delete两个 shared_ptr 各自以为自己是唯一的主人对象被释放两次。正确做法用make_shared或者第二个从第一个拷贝。2. 在成员函数里把 this 交给 shared_ptr。void Arm::Register() { g_Manager-Add(std::shared_ptrArm(this)); // ☠️ 又一个独立计数器 }外面那个 shared_ptr 不知道这个的存在又是 double delete。真有这个需求让类继承std::enable_shared_from_this用shared_from_this()。3. 循环引用不自知。 两个类互相shared_ptr持有程序退出时内存全漏。画个持有关系图发现环就把其中一边改 weak。4. 数组用错。std::shared_ptrint p(new int[10]); // ☠️ 析构调的是 delete 不是 delete[]数组用std::vector或者 C17 的shared_ptrint[]。5. get() 出来的裸指针乱存乱删。p.get()是借你看看不许 delete不许长期保存不许拿去构造新的智能指针。6. 以为 shared_ptr 万能到处用。 它解决的是「共享所有权」不是「我不想想所有权」。能 unique 就 unique代码读起来所有权一目了然。7. 跨线程乱用。 引用计数本身是线程安全的但指向的对象不是——两个线程同时读写对象内容该加的锁还是得加。智能指针管内存不管竞态。收尾一页纸记住全部智能指针 会自动 delete 的指针壳核心是 RAII释放绑在析构上出作用域自动释放。unique_ptr独占不许拷贝只能移动零开销默认首选。shared_ptr共享引用计数归零才释放用于「多个模块都要持有」的对象报警、事件、共享数据。weak_ptr旁观不计数用前先lock()问一句「还活着吗」专门破循环引用。三大纪律用make_unique/make_shared创建一个裸指针只进一个智能指针别把this随便包成 shared_ptr。一句话智能指针不是让你「不用管内存」而是让你把「谁负责」写进类型里——unique 是「我的」shared 是「大家的」weak 是「我只是看看」。