C++进阶:4. shared_ptr 现代C++内存管理的基石 【现代C指南】彻底掌握 std::shared_ptr从原理、用法到避坑全攻略本文是《现代C智能指针系列》的第二篇上一篇我们讲了独占式智能指针std::unique_ptr它是现代C的默认首选。但在实际开发中我们经常会遇到多个指针需要共享同一个对象的场景这时候unique_ptr就无能为力了。今天我们要讲的std::shared_ptr就是为了解决共享所有权问题而生的。它功能强大但也比unique_ptr复杂得多有很多容易踩的坑。读完这篇文章你将彻底搞懂shared_ptr的原理、用法、最佳实践和常见陷阱写出更安全、更高效的C代码。前言如果你问我“C中最容易被滥用的特性是什么”我会毫不犹豫地回答std::shared_ptr。很多C程序员学会shared_ptr之后就把所有指针都换成了shared_ptr觉得这样就“现代”了、“安全”了。但实际上滥用shared_ptr会导致性能下降、代码混乱甚至引入比裸指针更难调试的bug。shared_ptr是一把双刃剑它解决了共享对象的内存管理难题但它带来了引用计数的开销它有一个著名的致命陷阱循环引用所以学习shared_ptr最重要的不是学会怎么用它而是学会什么时候该用它什么时候绝对不要用它。一、为什么我们需要 shared_ptr在讲shared_ptr之前我们先看看unique_ptr解决不了的问题。1.1 unique_ptr 的局限性unique_ptr的核心是独占所有权一个对象只能有一个主人。这在大多数情况下是好事代码清晰、安全、零开销。但在有些场景下我们确实需要多个指针共享同一个对象一个配置对象被多个模块同时读取一个图片资源被多个UI控件同时使用观察者模式中多个观察者订阅同一个主题缓存系统中多个键指向同一个值这时候unique_ptr就不够用了因为它不能复制。1.2 裸指针共享的噩梦如果用裸指针来实现共享会发生什么classResource{};voidbad_example(){Resource*resnewResource();ModuleAa(res);// a 持有 resModuleBb(res);// b 也持有 res// 问题来了谁来 delete res// 如果 a 先 deleteb 就变成了野指针// 如果 b 先 deletea 就变成了野指针// 如果都不 delete就内存泄漏}这就是所有权混乱的问题当多个指针指向同一个对象时谁也不知道谁该负责释放它。1.3 shared_ptr 的解决方案shared_ptr引入了引用计数机制来解决这个问题每个shared_ptr对象都有一个关联的引用计数当你拷贝一个shared_ptr时引用计数加1当一个shared_ptr销毁时引用计数减1当引用计数减到0时自动释放对象这样最后一个持有对象的shared_ptr会负责释放它完美解决了共享对象的内存管理问题。二、shared_ptr 的核心原理理解shared_ptr的原理是正确使用它的前提很多坑都是因为不理解原理导致的。2.1 控制块Control Blockshared_ptr最核心的概念是控制块。当你创建一个shared_ptr时会在堆上分配一个控制块里面存储强引用计数当前有多少个shared_ptr指向这个对象弱引用计数当前有多少个weak_ptr指向这个对象删除器用于释放对象的函数分配器用于分配和释放控制块的分配器2.2 shared_ptr 的内存布局一个shared_ptr对象本身只包含两个指针指向实际对象的指针指向控制块的指针所以shared_ptr的大小是两个裸指针的大小在64位系统上是16字节而unique_ptr只有一个指针的大小8字节。2.3 引用计数的原子性为了保证线程安全引用计数的增减操作是原子的。这意味着多个线程同时拷贝和销毁shared_ptr是安全的不会出现计数错误。⚠️重要提示引用计数是线程安全的但对象本身不是线程安全的。多个线程同时修改同一个对象仍然需要加锁。三、shared_ptr 的基本用法3.1 创建 shared_ptr强烈推荐使用std::make_sharedC11 引入这是创建shared_ptr的标准方式。// ✅ 推荐写法创建一个管理int的shared_ptrautopstd::make_sharedint(10);// ❌ 不推荐写法直接用new初始化std::shared_ptrintp(newint(10));为什么推荐 make_shared性能更好make_shared会一次性分配对象控制块的内存而用new会分配两次一次对象一次控制块异常安全避免因异常导致内存泄漏代码更简洁不需要重复写类型避免裸指针暴露更安全3.2 拷贝和移动autop1std::make_sharedint(10);std::coutp1.use_count()std::endl;// 输出 1// 拷贝引用计数1autop2p1;std::coutp1.use_count()std::endl;// 输出 2std::coutp2.use_count()std::endl;// 输出 2// 移动引用计数不变p1变为空autop3std::move(p1);std::coutp1.use_count()std::endl;// 输出 0std::coutp3.use_count()std::endl;// 输出 23.3 访问对象shared_ptr的使用方式和unique_ptr、裸指针完全一样autoworkerstd::make_sharedWorker();// 使用箭头运算符访问成员worker-do_work();// 使用解引用运算符访问对象(*worker).do_work();// 获取底层裸指针尽量少用Worker*raw_ptrworker.get();3.4 释放对象shared_ptr会自动释放对象但你也可以手动释放autop1std::make_sharedint(10);autop2p1;// 计数2// 释放p1对对象的引用计数减为1p1.reset();// 释放p2对对象的引用计数减为0对象被释放p2.reset();3.5 获取引用计数可以用use_count()方法获取当前的引用计数autopstd::make_sharedint(10);std::coutp.use_count()std::endl;// 输出 1⚠️注意use_count()主要用于调试不要在业务逻辑中依赖它的值因为多线程环境下它可能随时变化。四、关键对比彻底搞懂 shared_ptr4.1 shared_ptr vs unique_ptr这是面试最常问的问题我做一个最全面的对比特性std::unique_ptrstd::shared_ptr所有权独占共享拷贝禁止拷贝只能移动可以拷贝大小1个裸指针8字节2个裸指针16字节额外开销零有引用计数和原子操作开销控制块无有删除器是类型的一部分类型擦除不是类型的一部分性能最高略低适用场景单一所有者多个所有者核心区别unique_ptr是零开销的而shared_ptr有不可忽视的性能开销。4.2 shared_ptr vs 裸指针特性std::shared_ptrT*内存管理自动释放必须手动delete所有权明确共享不明确内存泄漏不会除非循环引用极易发生重复释放不会极易发生野指针不会除非循环引用极易发生性能有开销无开销五、高级特性5.1 自定义删除器和unique_ptr一样shared_ptr也支持自定义删除器但有一个非常重要的区别unique_ptr的删除器是类型的一部分shared_ptr的删除器是类型擦除的不是类型的一部分// 自定义删除器autofile_deleter[](FILE*f){if(f){fclose(f);std::coutFile closedstd::endl;}};// shared_ptr 的类型是 std::shared_ptrFILE删除器不影响类型std::shared_ptrFILEfile(fopen(test.txt,w),file_deleter);这意味着不同删除器的shared_ptrT是同一个类型可以存放在同一个容器里这是shared_ptr比unique_ptr更灵活的地方。5.2 std::weak_ptr解决循环引用的神器std::weak_ptr是shared_ptr的配套工具专门用来解决循环引用问题。weak_ptr是一种弱引用它指向一个shared_ptr管理的对象但不增加引用计数。它的作用是观察对象是否还存在如果存在可以提升为shared_ptr如果不存在提升会失败autop1std::make_sharedint(10);std::weak_ptrintwpp1;// 弱引用计数不变还是1// 检查对象是否存在if(!wp.expired()){// 提升为shared_ptr计数变为2autop2wp.lock();std::cout*p2std::endl;}// p1销毁对象被释放p1.reset();// 现在对象已经不存在了if(wp.expired()){std::coutObject has been destroyedstd::endl;}5.3 从 unique_ptr 转换为 shared_ptr你可以很方便地把一个unique_ptr转换为shared_ptr因为unique_ptr是右值会触发shared_ptr的移动构造std::unique_ptrintupstd::make_uniqueint(10);std::shared_ptrintspstd::move(up);// ✅ 可以但反过来不行你不能把一个shared_ptr转换为unique_ptr因为shared_ptr是共享所有权的。六、shared_ptr 的最佳使用场景记住shared_ptr是最后的选择不是默认选择。只有当你明确需要共享所有权时才应该使用shared_ptr。以下是它的典型使用场景多个对象共享同一个资源// 一个配置对象被多个模块共享autoconfigstd::make_sharedConfig();ModuleAa(config);ModuleBb(config);观察者模式主题持有观察者的weak_ptr观察者持有主题的shared_ptr避免循环引用。缓存系统缓存中存储weak_ptr当对象不再被使用时自动从缓存中移除。跨线程共享对象多个线程同时访问同一个对象shared_ptr的引用计数是线程安全的。工厂模式返回值当工厂创建的对象需要被多个调用者共享时返回shared_ptr。七、常见误区和致命陷阱这是本文最重要的部分shared_ptr的大多数bug都来自这里。7.1 致命陷阱1循环引用这是shared_ptr最著名、最致命的问题。classB;// 前置声明classA{public:std::shared_ptrBb_ptr;~A(){std::coutA destroyedstd::endl;}};classB{public:std::shared_ptrAa_ptr;~B(){std::coutB destroyedstd::endl;}};voidmemory_leak(){autoastd::make_sharedA();autobstd::make_sharedB();a-b_ptrb;// a 持有 bb-a_ptra;// b 持有 a}// 离开作用域a和b都不会被销毁内存泄漏原因离开作用域时a销毁引用计数减为1因为b还持有它b销毁引用计数减为1因为a还持有它引用计数永远不会减到0对象永远不会被释放解决方法把其中一个shared_ptr换成weak_ptrclassB{public:std::weak_ptrAa_ptr;// 换成 weak_ptr~B(){std::coutB destroyedstd::endl;}};7.2 致命陷阱2同一个裸指针初始化多个 shared_ptrint*raw_ptrnewint(10);std::shared_ptrintp1(raw_ptr);// 计数1std::shared_ptrintp2(raw_ptr);// ❌ 致命错误后果p1和p2会创建两个独立的控制块它们的引用计数都是1当p1销毁时会释放对象当p2销毁时会再次释放同一个对象程序崩溃解决方法永远不要用同一个裸指针初始化多个shared_ptr。如果需要多个shared_ptr指向同一个对象拷贝第一个shared_ptr。7.3 致命陷阱3用 this 指针创建 shared_ptrclassA{public:std::shared_ptrAget_self(){returnstd::shared_ptrA(this);// ❌ 致命错误}};intmain(){autoastd::make_sharedA();autoba-get_self();return0;}// 重复释放程序崩溃原因和上面一样会创建两个独立的控制块。解决方法继承std::enable_shared_from_thisclassA:publicstd::enable_shared_from_thisA{public:std::shared_ptrAget_self(){returnshared_from_this();// ✅ 正确}};7.4 误区4过度使用 shared_ptr很多程序员把所有指针都换成shared_ptr这是非常错误的。shared_ptr有两倍的内存开销引用计数的增减是原子操作有性能开销共享所有权会让代码逻辑变得复杂难以理解记住优先用unique_ptr只有当你确实需要共享所有权时才用shared_ptr。7.5 误区5用 shared_ptr 管理数组C17 之前shared_ptr不支持数组默认会调用delete而不是delete[]导致内存泄漏。C17 及以后shared_ptr支持数组了但写法要注意// ✅ C17 正确写法std::shared_ptrint[]arr(newint[10]);// ✅ C20 更好的写法autoarrstd::make_sharedint[](10);八、总结和最佳实践8.1 核心总结shared_ptr是共享式智能指针基于引用计数实现它解决了共享对象的内存管理问题它有控制块和引用计数的开销它最著名的陷阱是循环引用用weak_ptr解决它是最后的选择不是默认选择8.2 最佳实践口诀优先用 unique_ptr需要共享才用 shared_ptr永远用 make_shared 创建 shared_ptr永远不要用同一个裸指针初始化多个 shared_ptr循环引用用 weak_ptr 解决不要在业务逻辑中依赖 use_count() 的值不要过度使用 shared_ptr8.3 最后一句话shared_ptr是一个强大的工具但也是一个危险的工具。正确使用它它会帮你解决复杂的内存管理问题滥用它它会给你带来比裸指针更难调试的bug。记住最好的指针是不需要指针。其次是 unique_ptr。最后才是 shared_ptr。C shared_ptr 高频面试题 校招/社招通用基础必考题80% 面试第一题1. 什么是 std::shared_ptr核心原理是什么标准答案std::shared_ptr是 C11 引入的共享式智能指针基于 RAII 机制和引用计数实现用于解决多个指针共享同一个对象的内存管理问题。核心原理每个shared_ptr指向一个堆上的控制块控制块存储强引用计数、弱引用计数、删除器、分配器拷贝shared_ptr时强引用计数原子加1销毁shared_ptr时强引用计数原子减1当强引用计数减到0时自动释放对象当弱引用计数也减到0时释放控制块2. shared_ptr 和 unique_ptr 的核心区别是什么标准答案两者最本质的区别是所有权模式不同特性std::unique_ptrstd::shared_ptr所有权独占所有权同一时间只能有一个持有者共享所有权多个持有者同时存在拷贝语义禁止拷贝只能通过std::move转移所有权支持拷贝拷贝时引用计数加1内存开销大小1个裸指针64位8字节零额外开销大小2个裸指针64位16字节有控制块开销性能最高和裸指针几乎一致略低引用计数增减是原子操作适用场景单一所有者默认首选多个所有者共享对象一句话总结优先用unique_ptr只有当需要共享所有权时才用shared_ptr。3. 为什么推荐用 std::make_shared 而不是直接用 new标准答案make_shared是创建shared_ptr的标准方式有4个不可替代的优势性能更高一次性分配对象控制块的连续内存而new会分两次分配先对象后控制块减少内存碎片和分配开销异常安全避免构造函数抛出异常时导致的内存泄漏代码更简洁不需要重复写类型避免裸指针暴露更安全从源头上杜绝“同一个裸指针初始化多个shared_ptr”的致命错误4. shared_ptr 的引用计数是线程安全的吗标准答案只有引用计数的增减操作是线程安全的对象本身的读写不是线程安全的。多个线程同时拷贝/销毁同一个shared_ptr是安全的因为引用计数用原子操作实现多个线程同时修改同一个shared_ptr指向的对象必须加锁同步⚠️ 这是最容易答错的题很多人会误以为shared_ptr整体是线程安全的。进阶必考题50% 面试会问5. 什么是循环引用shared_ptr 为什么会发生循环引用怎么解决标准答案循环引用是shared_ptr最著名的致命陷阱当两个或多个对象互相持有对方的shared_ptr时它们的引用计数永远不会减到0导致内存泄漏。示例classA{public:std::shared_ptrBb;};classB{public:std::shared_ptrAa;};voidleak(){autoastd::make_sharedA();autobstd::make_sharedB();a-bb;// a持有bb计数2b-aa;// b持有aa计数2}// 离开作用域a和b计数都减为1永远不会释放解决方法将其中一方的shared_ptr换成std::weak_ptr弱引用。weak_ptr指向对象但不增加强引用计数不会阻止对象释放。6. std::weak_ptr 是什么有什么作用标准答案std::weak_ptr是shared_ptr的配套工具是一种不拥有所有权的弱引用指针。核心作用解决循环引用问题这是它最主要的用途观察对象是否存在通过expired()方法判断对象是否已被释放安全访问对象通过lock()方法提升为shared_ptr如果对象已释放则返回空shared_ptr注意weak_ptr不能直接访问对象必须先提升为shared_ptr。7. 为什么不能用 this 指针直接创建 shared_ptr怎么正确获取指向自身的 shared_ptr标准答案如果直接用this创建shared_ptr会创建一个独立的新控制块导致同一个对象被两个不同的控制块管理最终发生重复释放、程序崩溃。正确方法让类继承std::enable_shared_from_thisT然后调用shared_from_this()方法。示例// 错误写法classBadA{public:std::shared_ptrBadAget_self(){returnstd::shared_ptrBadA(this);// ❌ 致命错误}};// 正确写法classGoodA:publicstd::enable_shared_from_thisGoodA{public:std::shared_ptrGoodAget_self(){returnshared_from_this();// ✅ 正确}};原理enable_shared_from_this内部有一个weak_ptr指向对象的控制块shared_from_this()会提升这个weak_ptr得到shared_ptr保证所有shared_ptr共享同一个控制块。8. shared_ptr 和 unique_ptr 的自定义删除器有什么区别标准答案两者都支持自定义删除器但实现方式完全不同unique_ptr的删除器是类型的一部分不同删除器的unique_ptrT是不同类型shared_ptr的删除器是类型擦除的不是类型的一部分不同删除器的shared_ptrT是同一个类型示例// unique_ptr删除器是类型的一部分std::unique_ptrFILE,decltype(fclose)fp1(fopen(a.txt,r),fclose);// shared_ptr删除器不影响类型std::shared_ptrFILEfp2(fopen(b.txt,r),fclose);这意味着shared_ptr更灵活不同删除器的shared_ptrT可以存放在同一个容器中。陷阱与细节题30% 面试会问9. 同一个裸指针可以初始化多个 shared_ptr 吗为什么标准答案绝对不可以如果用同一个裸指针初始化多个shared_ptr每个shared_ptr都会创建一个独立的控制块每个控制块的引用计数都是1。当第一个shared_ptr销毁时会释放对象当第二个shared_ptr销毁时会再次释放同一个对象导致程序崩溃。正确做法如果需要多个shared_ptr指向同一个对象拷贝第一个shared_ptr。10. shared_ptr 的 use_count() 方法有什么用可以在业务逻辑中依赖它吗标准答案use_count()返回当前的强引用计数仅用于调试和日志输出绝对不能在业务逻辑中依赖它的值。原因在多线程环境下use_count()的返回值可能在获取的瞬间就已经发生了变化无法保证准确性。11. shared_ptr 可以管理数组吗怎么正确使用标准答案C17 之前不直接支持数组默认会调用delete而不是delete[]导致内存泄漏C17 及以后支持数组特化版本自动调用delete[]正确写法// C17 及以上std::shared_ptrint[]arr1(newint[10]);// C20 及以上推荐autoarr2std::make_sharedint[](10);12. 可以把 unique_ptr 转换为 shared_ptr 吗反过来呢标准答案可以把unique_ptr转换为shared_ptrunique_ptr是右值会触发shared_ptr的移动构造转移所有权不可以把shared_ptr转换为unique_ptr因为shared_ptr是共享所有权的无法保证只有一个持有者示例std::unique_ptrintupstd::make_uniqueint(10);std::shared_ptrintspstd::move(up);// ✅ 可以