C++智能指针及易错场景 一、使用智能指针的好处智能指针基于 RAII 思想设计相比手动 new/delete 管理裸指针核心优势如下自动内存回收对象离开作用域时自动析构释放内存无需手动 delete从机制上避免内存泄漏。天然异常安全函数抛出异常时栈上智能指针对象会自动析构不会因异常跳过释放导致资源泄漏。所有权语义清晰unique_ptr 表达独占、shared_ptr 表达共享、weak_ptr 表达观测代码本身即体现资源归属关系。避免重复释放与野指针所有权转移、引用计数机制杜绝同一块内存多次释放weak_ptr 可安全判断对象是否已销毁。无缝适配标准容器可直接存入 vector、map 等容器容器销毁时元素自动析构无需手动遍历释放。C11已经 正式将三种智能指针纳入标准库memory头文件std::unique_ptr独占式、std::shared_ptr共享式、std::weak_ptr弱引用式取代了 C98 中设计有缺陷的std::auto_ptr。二、std::unique_ptr独占式智能指针核心特性unique_ptr对持有的内存拥有独占所有权同一时刻只能有一个unique_ptr指向同一块内存不允许拷贝构造和拷贝赋值仅支持移动语义转移所有权。它的开销极低默认情况下和裸指针大小一致没有额外性能损耗。实现原理内部封装裸指针析构函数中自动执行delete或自定义删除器。拷贝构造函数和拷贝赋值运算符被 delete显式禁用只保留移动构造和移动赋值转移所有权后原指针会被置为nullptr。基础用法1. 初始化推荐使用std::make_uniqueC14 引入异常安全且代码更简洁也可以直接用裸指针构造。#include memory #include iostream int main() { // 推荐make_unique 一次性完成内存分配和智能指针构造 std::unique_ptrint p1 std::make_uniqueint(10); // 不推荐手动 new 构造存在异常安全隐患 std::unique_ptrint p2(new int(20)); // 错误禁止拷贝构造 // std::unique_ptrint p3 p1; std::cout *p1 std::endl; // 解引用输出 10 return 0; } // 离开作用域p1、p2 自动析构内存自动释放2. 所有权转移移动语义通过std::move转移所有权转移后原指针变为空指针不可再解引用。std::unique_ptrint p1 std::make_uniqueint(10); std::unique_ptrint p2 std::move(p1); // 所有权转移给 p2 // 此时 p1 为 nullptrp2 管理内存 std::cout (p1 nullptr) std::endl; // 输出 1true std::cout *p2 std::endl; // 输出 103. 常用操作get()获取内部裸指针不释放所有权仅用于访问。reset()释放当前管理的内存可重新接管新的内存。release()放弃所有权返回裸指针需要手动管理释放。std::unique_ptrint p std::make_uniqueint(5); int* raw p.get(); // 获取裸指针不要手动 delete p.reset(new int(15)); // 释放旧内存管理新内存 p.reset(); // 释放内存指针置空 int* raw2 p.release(); // 放弃所有权返回裸指针 delete raw2; // 必须手动释放自定义删除器与数组支持unique_ptr的删除器是模板参数的一部分属于类型的组成部分编译期确定无运行时开销。管理数组时使用std::default_deleteT[]特化版本自动调用delete[]且支持[]下标访问。支持自定义函数、lambda、函数对象作为删除器。// 管理动态数组 std::unique_ptrint[] arr std::make_uniqueint[](5); arr[0] 1; arr[1] 2; // 自定义 lambda 删除器 auto custom_deleter [](int* p) { std::cout 自定义释放内存 std::endl; delete p; }; std::unique_ptrint, decltype(custom_deleter) p(new int(10), custom_deleter);适用场景函数内部的局部堆内存无需共享所有权。类的成员变量资源归类独有。容器中存储动态对象避免手动遍历释放。绝大多数默认场景优先选择unique_ptr性能开销为零。三、std::shared_ptr共享式智能指针核心特性shared_ptr支持多指针共享同一块内存通过引用计数机制管理生命周期每新增一个指向该内存的shared_ptr引用计数 1。每销毁一个shared_ptr引用计数 -1。当引用计数减为 0 时自动释放内存。实现原理每个shared_ptr对象内部包含两个指针数据指针指向实际的堆对象。控制块指针指向一个单独分配的控制块内部存储强引用计数shared count管理对象生命周期。弱引用计数weak count配合weak_ptr使用。删除器、内存分配器等信息。控制块的创建规则只有第一个shared_ptr构造时会创建控制块后续拷贝只会增加计数不会重复创建。基础用法1. 初始化推荐使用std::make_sharedC11 引入它会一次性分配对象和控制块的内存性能更高且异常安全。#include memory #include iostream int main() { // 推荐make_shared 一次分配性能更优 std::shared_ptrint p1 std::make_sharedint(10); // 手动构造两次内存分配对象 控制块 std::shared_ptrint p2(new int(20)); // 拷贝构造引用计数 1 std::shared_ptrint p3 p1; std::cout p1.use_count() std::endl; // 输出 2p1、p3 共同指向 std::cout *p3 std::endl; // 输出 10 return 0; } // 所有指针析构计数归0内存释放2. 常用操作use_count()返回当前强引用计数仅用于调试不建议用于业务逻辑判断。unique()判断是否为唯一持有者use_count() 1。reset()释放当前引用计数 - 1可重新指向新内存。get()获取裸指针。自定义删除器与unique_ptr不同shared_ptr的删除器不是模板参数而是通过类型擦除存储在控制块中属于运行期动态特性。因此同一个类型的shared_ptr可以携带不同的删除器灵活性更高但有轻微运行时开销。// 自定义删除器类型仍为 std::shared_ptrint std::shared_ptrint p(new int(10), [](int* ptr) { std::cout 自定义释放 std::endl; delete ptr; }); // C17 起支持原生数组管理 std::shared_ptrint[] arr(new int[5]); arr[0] 1;线程安全shared_ptr的线程安全需要分两层理解引用计数的增减是线程安全的多个线程同时拷贝 / 销毁指向同一对象的不同shared_ptr对象引用计数的原子操作不会出现竞争。智能指针对象本身的修改不是线程安全的多个线程同时修改同一个shared_ptr对象如reset、赋值、移动会出现数据竞争需要手动加锁保护。适用场景多个对象需要共享同一份资源的场景。观察者模式、缓存系统、跨模块资源传递等。资源生命周期不明确需要由最后一个使用者释放的场景。四、std::weak_ptr弱引用智能指针核心特性weak_ptr是shared_ptr的辅助工具不拥有内存所有权仅作为 “观察者” 存在它指向shared_ptr管理的对象但不会增加强引用计数。不会阻止内存释放专门用于解决shared_ptr的循环引用问题。实现原理weak_ptr同样指向控制块会增加控制块中的弱引用计数强引用计数归 0 时对象内存立即释放。控制块需要等弱引用计数也归 0 时才会释放。基础用法1. 构造与状态判断std::shared_ptrint sp std::make_sharedint(10); std::weak_ptrint wp sp; // 用 shared_ptr 构造 weak_ptr // expired()判断对象是否已被释放 std::cout wp.expired() std::endl; // 输出 0false对象存在 // lock()获取对应的 shared_ptr对象存在则返回有效 shared_ptr否则返回空 std::shared_ptrint sp2 wp.lock(); if (sp2) { std::cout *sp2 std::endl; // 输出 10 }核心作用解决循环引用shared_ptr最大的缺陷是循环引用两个对象互相持有对方的shared_ptr导致引用计数永远无法归 0造成内存泄漏。循环引用示例struct B; // 前置声明 struct A { std::shared_ptrB b_ptr; ~A() { std::cout A 析构 std::endl; } }; struct B { std::shared_ptrA a_ptr; ~B() { std::cout B 析构 std::endl; } }; int main() { std::shared_ptrA a std::make_sharedA(); std::shared_ptrB b std::make_sharedB(); a-b_ptr b; // a 持有 bb 的引用计数为 2 b-a_ptr a; // b 持有 aa 的引用计数为 2 return 0; } // 离开作用域a、b 计数各减 1最终都为 1永远不会析构内存泄漏解决方案将其中一个改为 weak_ptrstruct B { std::weak_ptrA a_ptr; // 改为弱引用不增加 a 的引用计数 ~B() { std::cout B 析构 std::endl; } };此时离开作用域时a先析构强引用计数减为 0A 对象释放。A 析构时内部的b_ptr析构b 的强引用计数减为 0B 对象释放。循环被打破内存正常释放。适用场景打破shared_ptr的循环引用。缓存、观察者模式只需要观测对象是否存在不需要持有所有权。避免悬空指针安全地访问可能已释放的对象。五大易错场景与避坑易错场景1同一裸指针初始化多个智能指针后果重复释放double free程序崩溃。cppint* raw new int(10);std::unique_ptrint up1(raw);std::unique_ptrint up2(raw); // 两个指针都认为自己独占该内存析构时各释放一次避坑优先使用 make_unique / make_shared从源头避免裸指针复用shared_ptr 通过拷贝增加计数禁止重复用裸指针构造。易错场景2shared_ptr 循环引用导致内存泄漏后果引用计数永远无法降为 0内存永久泄漏。cppstruct Node {std::shared_ptrNode next;};auto a std::make_sharedNode();auto b std::make_sharedNode();a-next b;b-next a; // 双向强引用形成循环计数始终为1避坑双向关联场景中观测方/从属侧必须使用 weak_ptr 打破循环不增加强引用计数。易错场景3手动 delete get() 返回的裸指针后果重复释放智能指针析构时会再次执行 delete。cppauto up std::make_uniqueint(10);int* raw up.get();delete raw; // 手动释放后up 析构时会再次释放避坑get() 仅返回观测用裸指针不转移所有权如需主动放弃所有权使用 release() 方法。易错场景4enable_shared_from_this 使用错误后果抛出 bad_weak_ptr 异常或创建多个控制块导致重复释放。cpp// 错误1栈对象调用 shared_from_this()class Foo : public std::enable_shared_from_thisFoo {};Foo stack_obj;auto sp stack_obj.shared_from_this(); // 无对应控制块抛异常// 错误2直接用 this 构造 shared_ptr 返回std::shared_ptrFoo get_self() {return std::shared_ptrFoo(this); // 每次创建新控制块重复释放}避坑继承 enable_shared_from_this 的类对象必须先被 shared_ptr 管理才能调用 shared_from_this()建议将构造函数设为私有配合工厂函数返回 shared_ptr。易错场景5用栈内存地址初始化智能指针后果未定义行为析构时对栈内存执行 delete大概率程序崩溃。cppint val 10; // 栈上变量std::unique_ptrint up(val); // 析构时 delete 栈内存行为未定义避坑仅对堆内存new 分配使用智能指针栈对象直接用普通变量即可永远不要用栈对象、全局对象、静态对象的地址构造智能指针。