C++智能指针:从RAII原理到shared_ptr实现与循环引用解决方案
1. 项目概述从手动管理到智能托管的资源革命在C的世界里摸爬滚打多年最让人头疼的莫过于内存管理。一个new对应一个delete看似简单但在复杂的函数调用、异常抛出和多线程环境下稍有不慎就会导致内存泄漏、悬空指针或者重复释放这些Bug往往隐蔽且致命。C11标准引入的智能指针可以说是一场“资源管理”的革命它并非一个全新的概念而是将业界久经考验的RAIIResource Acquisition Is Initialization原则通过标准库的形式固化下来提供了一套开箱即用的解决方案。今天我们就来深入聊聊这个话题不仅仅是会用std::shared_ptr更要搞懂它背后的RAII哲学、各种智能指针的适用场景以及最经典的“循环引用”问题及其解法。无论你是正在学习C11特性的新手还是想巩固底层原理的老手这篇文章都将带你从“知其然”到“知其所以然”最后我们甚至会动手模拟实现一个简易版的shared_ptr彻底吃透它。2. RAII原则C资源管理的基石在深入智能指针之前我们必须先理解它的灵魂——RAII原则。这个原则听起来很高大上但核心理念异常朴素和强大资源的生命周期与对象的生命周期严格绑定。2.1 RAII的核心思想与运作机制简单来说RAII要求我们在对象的构造函数中获取资源在对象的析构函数中释放资源。这样只要对象本身的生命周期是受控的例如在栈上创建离开作用域时自动析构那么它持有的资源也必然是受控的。这利用了C语言的一个基本保证无论函数是正常返回还是因为异常而提前退出栈上局部对象的析构函数都会被调用。我们来看一个最经典的、非智能指针的例子文件操作。// 非RAII风格 - 危险 void processFile() { FILE* fp fopen(data.txt, r); if (!fp) return; // 打开失败直接返回文件句柄泄漏 // ... 一些可能抛出异常的操作 ... fclose(fp); // 如果上面抛异常这句不会执行再次泄漏 }上面的代码有两个潜在的资源泄漏点打开失败时直接返回以及中间操作抛出异常。现在我们用RAII思想封装一下// RAII风格封装文件句柄 class FileHandle { public: explicit FileHandle(const char* filename, const char* mode) : fp_(fopen(filename, mode)) { if (!fp_) { throw std::runtime_error(Failed to open file); } } ~FileHandle() { if (fp_) { fclose(fp_); } } // 禁用拷贝防止重复释放或实现移动语义 FileHandle(const FileHandle) delete; FileHandle operator(const FileHandle) delete; // 提供获取原始指针的接口谨慎使用 FILE* get() const { return fp_; } private: FILE* fp_; }; void processFileSafe() { FileHandle fh(data.txt, r); // 资源在构造函数中获取 // 使用 fh.get() 进行文件操作 // ... 即使这里抛出异常 ... } // 离开作用域时fh的析构函数自动调用关闭文件。资源在析构函数中释放。通过FileHandle这个类我们将不透明的FILE*资源与对象的生命周期绑定。processFileSafe函数无需关心何时调用fclose因为析构函数会替我们完成。这就是RAII的威力将容易出错的手动资源管理转化为由编译器自动保证的、确定性的资源释放。注意在实现RAII类时需要仔细考虑拷贝语义。对于文件句柄这类不可复制的资源通常需要禁用拷贝构造函数和拷贝赋值运算符C11前声明为privateC11后使用 delete或者实现移动语义Move Semantics将资源所有权转移给新对象避免多个对象试图释放同一份资源。2.2 RAII与异常安全RAII是实现强异常安全保证的关键手段。异常安全通常有几个级别无保证发生异常时程序可能处于任何状态。基本保证发生异常时程序状态保持不变所有资源被正确清理但数据内容可能改变。强保证操作要么完全成功要么完全失败程序状态如同操作从未发生。不抛异常保证操作承诺绝不抛出异常。使用RAII包装资源后即使操作序列中发生异常栈展开stack unwinding过程也会依次调用已构造对象的析构函数从而确保所有已获取的资源都被释放。这使得编写异常安全的代码变得简单许多。智能指针正是将RAII应用于动态内存这一最常见资源的管理工具。3. C11智能指针家族详解C11在memory头文件中提供了三种主要的智能指针std::unique_ptr、std::shared_ptr和std::weak_ptr。它们分工明确解决了不同场景下的资源管理问题。3.1std::unique_ptr独占所有权的守卫unique_ptr如其名独占所指对象的所有权。它删除了拷贝构造函数和拷贝赋值运算符只支持移动语义。这意味着同一时刻只有一个unique_ptr可以指向一个给定的对象。当这个unique_ptr被销毁例如离开作用域它所指向的对象也会被自动删除。核心特性与使用场景轻量高效开销极小通常只包含一个原始指针。在大多数情况下可以替代需要手动delete的原始指针且无额外性能损失。自定义删除器可以指定一个函数或函数对象在析构时执行特定的清理操作如对于new[]分配的数组或使用fclose关闭文件。适用场景适用于资源所有权清晰、无需共享的场景。例如在工厂函数中返回动态创建的对象或者作为类的成员变量表示该类独占某个资源。#include memory #include iostream class Widget { public: Widget() { std::cout Widget constructed\n; } ~Widget() { std::cout Widget destroyed\n; } void doSomething() { std::cout Widget working\n; } }; std::unique_ptrWidget createWidget() { // 工厂函数返回独占指针 return std::make_uniqueWidget(); // C14起更安全高效 } int main() { { std::unique_ptrWidget up1 std::make_uniqueWidget(); // std::unique_ptrWidget up2 up1; // 错误不能拷贝 std::unique_ptrWidget up2 std::move(up1); // 正确所有权转移 // 此时 up1 为空up2 拥有对象 if (up2) { up2-doSomething(); // 使用 - 操作符访问成员 } } // up2 离开作用域Widget对象被自动销毁 return 0; }实操心得优先使用std::make_uniqueC14来创建unique_ptr。原因有三1) 异常安全。make_unique将对象构造和智能指针构造合并为一个原子操作避免了因构造顺序可能导致的资源泄漏。2) 代码更简洁。3) 可能带来轻微的性能提升减少一次内存分配。对于shared_ptr也有对应的std::make_shared。3.2std::shared_ptr共享所有权的管家当多个实体需要“共享”同一个对象且无法确定谁该最后负责删除它时shared_ptr就派上用场了。它通过引用计数来实现共享所有权。每个shared_ptr对象内部除了存储原始指针还指向一个控制块control block控制块中至少包含一个引用计数器。当一个新的shared_ptr通过拷贝构造或拷贝赋值与另一个shared_ptr指向同一对象时引用计数加1。当某个shared_ptr被销毁或重置时引用计数减1。当引用计数变为0时控制块负责删除所管理的对象。核心特性与使用场景共享所有权多个shared_ptr可以指向同一个对象。线程安全shared_ptr的引用计数增减操作是原子操作因此从多个线程拷贝/销毁指向同一对象的shared_ptr是安全的。但请注意这并不保证所指向对象本身是线程安全的。循环引用风险这是shared_ptr最著名的陷阱后文会详细讨论。适用场景适用于需要共享动态生命周期对象所有权的场景例如存储在标准容器中的对象、缓存系统、观察者模式等。#include memory #include vector class Node { public: std::vectorstd::shared_ptrNode children; std::shared_ptrNode parent; // 潜在循环引用 }; void sharedPtrExample() { auto sp1 std::make_sharedint(42); // 引用计数 1 { auto sp2 sp1; // 拷贝构造引用计数 2 auto sp3 sp1; // 拷贝构造引用计数 3 } // sp2和sp3离开作用域析构引用计数 1 // 此时只有sp1指向对象引用计数 1 } // sp1离开作用域引用计数减为0对象被删除3.3std::weak_ptr打破循环的观察者weak_ptr是shared_ptr的“弱”引用。它指向一个由shared_ptr管理的对象但不增加该对象的引用计数。这意味着weak_ptr的存在不会阻止其所指对象被销毁。你可以把weak_ptr想象成一张“门票”它允许你在对象还存在的时候访问它但它不负责保管对象。核心特性与使用场景不增加引用计数解决shared_ptr循环引用的关键。需要配合shared_ptr使用weak_ptr必须从一个shared_ptr或另一个weak_ptr构造而来。访问对象不能直接通过weak_ptr访问对象需要先调用lock()方法将其转换为一个临时的shared_ptr。如果对象还存在lock()返回一个有效的shared_ptr增加引用计数如果对象已被释放则返回一个空的shared_ptr。适用场景打破循环引用在可能形成循环引用的结构中如双向链表、树形结构中的父节点引用将其中一方的引用改为weak_ptr。缓存缓存中存储weak_ptr当需要时尝试提升lock()。如果对象还在被其他shared_ptr持有则使用如果已被释放则重新加载。这避免了缓存阻止对象被正常释放。观察者模式主题Subject持有观察者Observer的weak_ptr避免观察者意外地延长主题的生命周期。#include memory #include iostream class B; // 前向声明 class A { public: std::shared_ptrB b_ptr; ~A() { std::cout A destroyed\n; } }; class B { public: // 使用 weak_ptr 打破循环引用 std::weak_ptrA a_ptr; ~B() { std::cout B destroyed\n; } }; void weakPtrExample() { auto a std::make_sharedA(); auto b std::make_sharedB(); a-b_ptr b; b-a_ptr a; // 这里是 weak_ptr 赋值不会增加A的引用计数 // 尝试通过 weak_ptr 访问对象 if (auto tmp b-a_ptr.lock()) { // 提升为 shared_ptr std::cout A is still alive\n; } else { std::cout A has been destroyed\n; } } // a和b离开作用域引用计数都能正常减到0A和B对象都被正确销毁。4. 深入shared_ptr模拟实现与原理剖析要真正理解shared_ptr没有什么比自己动手实现一个简化版本更好的方法了。我们将实现一个MySharedPtr包含核心的引用计数功能。注意为了简化我们省略了线程安全、自定义删除器、别名构造等高级特性。4.1 控制块设计与成员变量shared_ptr的核心是共享的控制块。控制块至少需要存储指向被管理对象的指针。引用计数强引用计数use_count。弱引用计数用于weak_ptr我们简化版先不考虑。我们的MySharedPtr将包含两个数据成员T* ptr_: 指向实际管理的对象。int* count_: 指向引用计数的指针。使用指针是为了让所有共享同一对象的MySharedPtr实例能操作同一个计数器。templatetypename T class MySharedPtr { private: T* ptr_; // 原始指针 int* count_; // 指向引用计数的指针 // ... 后续方法实现 };4.2 构造函数、拷贝控制与析构函数这是智能指针行为的关键。1. 构造函数从原始指针构造explicit MySharedPtr(T* p nullptr) : ptr_(p), count_(nullptr) { if (ptr_) { count_ new int(1); // 分配控制块初始化引用计数为1 } }注意构造函数声明为explicit防止隐式转换。例如避免MySharedPtrint p new int(5);这种容易出错的写法强制使用MySharedPtrint p(new int(5));。2. 拷贝构造函数MySharedPtr(const MySharedPtr other) : ptr_(other.ptr_), count_(other.count_) { if (count_) { (*count_); // 共享对象引用计数加1 } }这是实现共享所有权的核心。拷贝时只复制指针并递增同一个引用计数。3. 拷贝赋值运算符MySharedPtr operator(const MySharedPtr other) { // 处理自赋值p p; if (this ! other) { // 先清理当前持有的资源 release(); // 再接管新资源 ptr_ other.ptr_; count_ other.count_; if (count_) { (*count_); } } return *this; }赋值操作需要先释放当前对象可能持有的旧资源通过release辅助函数再接管新资源。自赋值检查是必须的否则在自赋值时release()可能会错误地提前释放资源。4. 析构函数~MySharedPtr() { release(); }析构函数调用release()来减少引用计数并在计数为0时销毁对象。5. 关键的release()辅助函数private: void release() { if (count_) { --(*count_); // 减少引用计数 if (*count_ 0) { // 如果我是最后一个持有者 delete ptr_; // 销毁对象 delete count_; // 销毁控制块引用计数 ptr_ nullptr; count_ nullptr; } } }这个函数封装了资源释放的逻辑。它减少引用计数并判断自己是否是最后一个“管家”。如果是则履行最终职责删除对象和释放控制块内存。4.3 基础功能实现为了让我们的MySharedPtr像指针一样工作需要重载一些运算符。// 解引用操作符 T operator*() const { // 应有检查这里简化。标准库的 shared_ptr 在 ptr_ 为空时解引用是未定义行为。 return *ptr_; } // 箭头操作符 T* operator-() const { return ptr_; } // 获取原始指针谨慎使用 T* get() const { return ptr_; } // 判断是否为空 explicit operator bool() const { return ptr_ ! nullptr; } // 获取引用计数 int use_count() const { return count_ ? *count_ : 0; }4.4 一个简单的测试#include iostream // 将上述 MySharedPtr 的实现放在这里 class Test { public: Test() { std::cout Test()\n; } ~Test() { std::cout ~Test()\n; } void greet() { std::cout Hello from Test!\n; } }; int main() { std::cout Creating sp1...\n; MySharedPtrTest sp1(new Test()); // 计数1 { std::cout use_count: sp1.use_count() \n; // 1 std::cout Creating sp2 via copy...\n; MySharedPtrTest sp2 sp1; // 拷贝构造计数2 sp2-greet(); // 使用 - 操作符 std::cout use_count: sp1.use_count() \n; // 2 std::cout Leaving inner scope...\n; } // sp2析构计数减为1 std::cout use_count: sp1.use_count() \n; // 1 std::cout Leaving main scope...\n; } // sp1析构计数减为0Test对象被销毁输出 ~Test()通过这个简单的模拟实现你应该能清晰地看到shared_ptr内部引用计数是如何协同工作并自动管理对象生命周期的。当然工业级的std::shared_ptr远比这复杂它需要考虑线程安全使用原子操作、弱引用计数、自定义删除器、类型擦除、别名构造等但核心原理与此一致。5. 循环引用问题成因、诊断与解决方案这是使用shared_ptr时最经典的陷阱也是面试高频考点。理解了它你对智能指针所有权模型的认识会上一个台阶。5.1 什么是循环引用循环引用发生在两个或更多个对象通过shared_ptr相互引用形成一个环。由于环中每个对象的引用计数都至少为1被环内的其他对象引用着导致即使外部没有任何shared_ptr指向这个环环内对象的引用计数也无法降为0从而造成内存泄漏。让我们用之前提到的父子节点例子但这次双方都用shared_ptr#include memory #include iostream class BadNode { public: std::shared_ptrBadNode partner; ~BadNode() { std::cout BadNode destroyed\n; } }; void circularReferenceDemo() { std::cout Creating nodeA and nodeB...\n; auto nodeA std::make_sharedBadNode(); // nodeA 计数 1 auto nodeB std::make_sharedBadNode(); // nodeB 计数 1 nodeA-partner nodeB; // nodeB 计数 2 (nodeB 和 nodeA-partner) nodeB-partner nodeA; // nodeA 计数 2 (nodeA 和 nodeB-partner) std::cout nodeA use_count: nodeA.use_count() \n; // 输出 2 std::cout nodeB use_count: nodeB.use_count() \n; // 输出 2 std::cout Leaving scope...\n; } // 函数结束栈上的 nodeA 和 nodeB 被销毁。 // nodeA 销毁nodeA 的引用计数从2减为1 (还剩 nodeB-partner 指向它) // nodeB 销毁nodeB 的引用计数从2减为1 (还剩 nodeA-partner 指向它) // 此时两个对象的引用计数都为1它们相互指着对方无法被删除 // 控制台不会输出 BadNode destroyed内存泄漏发生。运行这段代码你会发现析构函数没有被调用程序静默地泄漏了内存。5.2 如何诊断循环引用在大型项目中循环引用可能隐藏得很深。以下是一些诊断方法代码审查仔细检查类设计中持有shared_ptr的成员变量特别是存在双向关联如父子、所有者-从属、观察者-被观察者的情况。使用工具Valgrind (Linux/Mac)强大的内存调试工具可以检测出“确定丢失”definitely lost的内存循环引用通常属于此类。AddressSanitizer (ASan)编译时插桩工具能检测多种内存错误结合LeakSanitizer可以检测内存泄漏。智能指针调试一些IDE或调试器可以显示shared_ptr的引用计数异常高的计数可能暗示着循环引用。分析引用关系在头脑中或纸上画出对象间的引用关系图检查是否存在闭环。5.3 解决方案使用weak_ptr打破循环解决方案的核心在于将环中的至少一个引用从“强引用”shared_ptr改为“弱引用”weak_ptr。weak_ptr不会增加引用计数因此不会阻止对象被销毁。修改上面的BadNode例子class GoodNode { public: std::shared_ptrGoodNode partner; // 假设这里必须是 shared_ptr // 另一方使用 weak_ptr std::weak_ptrGoodNode backReference; // 用于指回对方 ~GoodNode() { std::cout GoodNode destroyed\n; } }; void solvedCircularReferenceDemo() { std::cout Creating goodNodeA and goodNodeB...\n; auto nodeA std::make_sharedGoodNode(); auto nodeB std::make_sharedGoodNode(); nodeA-partner nodeB; // nodeB 计数 2 nodeB-partner nodeA; // nodeA 计数 2 // 设置弱引用 nodeA-backReference nodeB; // nodeB 计数仍为 2 (weak_ptr 不增加计数) nodeB-backReference nodeA; // nodeA 计数仍为 2 std::cout nodeA use_count: nodeA.use_count() \n; // 2 std::cout nodeB use_count: nodeB.use_count() \n; // 2 std::cout Leaving scope...\n; } // 栈上 nodeA, nodeB 销毁。 // nodeA 销毁nodeA 计数从2减为1 (还剩 nodeB-partner) // nodeB 销毁nodeB 计数从2减为1 (还剩 nodeA-partner) // 注意此时 nodeA-partner 和 nodeB-partner 仍然相互持有 shared_ptr。 // 我们并没有打破 partner 之间的强引用环内存依然泄漏等等这个例子有问题我们只把新增的backReference改成了weak_ptr但原本形成环的partner成员依然是shared_ptr所以循环引用依然存在。正确的做法是分析所有权关系将环中代表“非拥有性”观察关系的引用改为weak_ptr。假设我们重新设计GoodNode有一个“主人”owner但主人不需要拥有“下属”class Owner; class Member { public: std::weak_ptrOwner myOwner; // 成员不拥有主人只是观察/引用 ~Member() { std::cout Member destroyed\n; } }; class Owner { public: std::shared_ptrMember myMember; // 主人拥有成员 ~Owner() { std::cout Owner destroyed\n; } }; void correctSolutionDemo() { auto owner std::make_sharedOwner(); auto member std::make_sharedMember(); owner-myMember member; // member 计数 2 (member 和 owner-myMember) member-myOwner owner; // owner 计数 1 (owner weak_ptr 不增加计数) std::cout owner use_count: owner.use_count() \n; // 1 std::cout member use_count: member.use_count() \n; // 2 // 当离开作用域时 // 1. 栈上 owner 销毁owner 引用计数从1减为0 - Owner对象被销毁。 // 2. Owner对象销毁导致其成员 myMember (shared_ptrMember) 销毁。 // 3. myMember 销毁使得 member 的引用计数从2减为1。 // 4. 栈上 member 销毁member 引用计数从1减为0 - Member对象被销毁。 // 完美析构 }关键点设计类时必须清晰定义对象间的所有权关系。谁“拥有”谁拥有关系用shared_ptr观察/引用关系用weak_ptr。在双向关系中通常一方为拥有者shared_ptr另一方为被拥有者或观察者weak_ptr。5.4 其他处理循环引用的策略重新设计所有权结构这是最根本的方法。思考是否真的需要双向的shared_ptr能否将关系改为单向或者使用原始指针/引用作为观察端前提是你能确保观察端不会在对象销毁后被访问。手动打破循环在知道不再需要环状引用时手动将其中一个shared_ptr重置reset()为nullptr。但这要求对生命周期有精确控制容易出错。使用std::enable_shared_from_this当一个对象需要将自己作为shared_ptr传递给其他对象时例如在回调函数中可以使用这个基类来安全地获取指向自身的shared_ptr但这本身不解决循环引用只是提供了获取shared_ptr的正确方式。6. 常见问题与排查技巧实录在实际项目中使用智能指针总会遇到一些意想不到的情况。这里记录几个我踩过的坑和对应的排查思路。6.1 问题一混用new和make_shared导致的内存泄漏场景一个对象由make_shared创建但其某个成员需要自定义删除器于是又用new创建了一个shared_ptr指向同一对象。auto sp1 std::make_sharedMyObject(); // ... 后来由于某些原因错误地又创建了一个 shared_ptr std::shared_ptrMyObject sp2(sp1.get()); // 灾难sp2根据一个原始指针创建了一个新的shared_ptr它拥有自己的控制块。当sp1和sp2都销毁时它们会各自尝试删除MyObject导致重复释放引发未定义行为通常是程序崩溃。排查技巧永远不要从一个原始指针创建多个独立的shared_ptr。如果需要从原始指针构造shared_ptr确保这是该对象的第一个也是唯一一个shared_ptr。更佳实践是始终使用make_shared或make_unique来创建智能指针。如果必须使用原始指针例如来自第三方库立即将其封装到智能指针中并且不再保留该原始指针。6.2 问题二在函数参数中盲目传递shared_ptr场景函数只需要使用对象并不需要共享所有权。void processWidget(std::shared_ptrWidget sp); // 按值传递 auto myWidget std::make_sharedWidget(); processWidget(myWidget); // 调用时会发生拷贝增加引用计数按值传递shared_ptr会触发拷贝构造增加引用计数函数退出时减少引用计数。如果函数本身不需要延长对象的生命周期即不需要拥有所有权这就是不必要的开销并且可能模糊了接口的意图。解决方案如果函数只需要访问对象不涉及所有权传递Widget*原始指针或Widget引用。明确告知调用者函数不会管理对象的生命周期。如果函数需要存储这个shared_ptr共享所有权按值传递。这明确表示了所有权的转移/共享。如果函数可能需要存储也可能不需要传递const std::shared_ptrWidget常量引用。这避免了拷贝开销但函数内部不能直接存储它需要拷贝一份。接口意图稍显模糊。实操心得在API设计时仔细考虑所有权语义。按值传递shared_ptr是昂贵的操作涉及原子操作应仅在需要共享所有权时使用。对于只读访问优先使用*或。6.3 问题三this指针的陷阱场景在类的成员函数中需要将当前对象this传递给一个接受shared_ptr的函数。class NetworkHandler { public: void startAsync() { // 假设 scheduler 是一个接受 shared_ptrNetworkHandler 的回调调度器 // scheduler.schedule(shared_from_this()); // 错误不能直接使用 this } }; auto handler std::make_sharedNetworkHandler(); handler-startAsync();你不能直接从this指针构造一个shared_ptr因为this对象可能并不是由shared_ptr管理的或者即使它是直接构造也会创建一个新的、独立的控制块导致重复释放。解决方案让类继承自std::enable_shared_from_thisT并使用shared_from_this()成员函数。class NetworkHandler : public std::enable_shared_from_thisNetworkHandler { public: void startAsync() { auto self shared_from_this(); // 正确获取指向自身的 shared_ptr scheduler.schedule(self); } };重要前提对象必须已经由一个shared_ptr管理即已经存在于某个shared_ptr中才能调用shared_from_this()。否则会抛出std::bad_weak_ptr异常。通常这意味着对象的创建必须通过std::make_shared或直接构造shared_ptr而不能在栈上创建。6.4 问题速查表问题现象可能原因排查方向与解决方案程序崩溃如double free同一原始指针被多个独立的shared_ptr管理。检查代码确保每个对象只由一个shared_ptr控制块管理。使用make_shared创建。内存持续增长不释放循环引用。使用内存分析工具Valgrind, ASan定位泄漏点。检查类设计将非拥有性引用改为weak_ptr。对象意外被提前释放shared_ptr在某个地方被意外重置或超出了作用域。检查所有shared_ptr的生命周期。确认在需要对象存活的上下文中有shared_ptr持有它。多线程下访问对象崩溃shared_ptr的引用计数是线程安全的但对象本身不是。多个线程通过不同的shared_ptr副本修改同一对象。需要对对象本身进行同步保护如使用互斥锁std::mutex。性能开销较大过度使用shared_ptr特别是按值传递。原子操作带来开销。重新评估所有权模型能用unique_ptr或原始指针/引用的地方就不要用shared_ptr。避免不必要的拷贝。7. 进阶话题与最佳实践7.1make_sharedvsshared_ptr构造函数我们一直强调优先使用make_shared这里详细对比一下std::make_sharedT(args...)优点异常安全例如process(std::shared_ptrWidget(new Widget), computeValue())如果computeValue()抛出异常new Widget分配的内存可能泄漏。而process(std::make_sharedWidget(), computeValue())是安全的。效率通常只需一次内存分配将对象和控制块分配在连续内存中而newshared_ptr构造需要两次。缺点无法指定自定义删除器。如果对象很大且weak_ptr存活时间远长于所有shared_ptr由于对象和控制块内存连续可能无法及时释放对象占用的那部分内存控制块需要等待所有weak_ptr释放。std::shared_ptrT(new T(args...))用于需要自定义删除器或需要先new再构造shared_ptr的特殊场景。结论在绝大多数不需要自定义删除器的场景下无条件使用make_shared。7.2 智能指针与多线程shared_ptr的线程安全多个线程同时读写同一个shared_ptr实例是非线程安全的需要加锁。但多个线程拷贝/销毁指向同一对象的不同shared_ptr实例是安全的引用计数操作是原子的。指向的对象shared_ptr的线程安全不保护其指向的对象。如果多个线程需要通过shared_ptr访问同一对象必须对对象本身进行同步。weak_ptr.lock()这是一个原子操作它检查对象是否存在如果存在则创建一个shared_ptr。这常用于实现线程安全的对象缓存访问。7.3 智能指针与标准容器将智能指针放入标准容器如std::vectorstd::shared_ptrWidget是非常常见且正确的用法。这避免了容器析构时内存泄漏的问题。但要注意使用emplace_back或push_back(std::make_shared...(...))来直接构造元素避免临时对象。如果容器存储的是unique_ptr由于unique_ptr不可拷贝只能移动所以向容器添加元素需要使用std::move或者使用emplace_back直接构造。std::vectorstd::shared_ptrWidget widgets; widgets.reserve(10); for (int i 0; i 10; i) { widgets.push_back(std::make_sharedWidget(i)); // 好 // widgets.emplace_back(std::make_sharedWidget(i)); // 也可以 } std::vectorstd::unique_ptrWidget uniqueWidgets; uniqueWidgets.push_back(std::make_uniqueWidget(1)); // 错误unique_ptr不能拷贝 uniqueWidgets.push_back(std::move(std::make_uniqueWidget(2))); // 正确但效率低 uniqueWidgets.emplace_back(std::make_uniqueWidget(3)); // 最佳直接构造在容器内7.4 何时使用原始指针或引用智能指针不是万能的原始指针和引用仍有其用武之地不涉及所有权的观察当一个函数或对象只需要使用另一个对象而不负责其生命周期时使用原始指针T*或引用T。这明确了“我只是借用不负责管理”的语义。例如遍历容器、访问对象成员。性能关键路径在性能极其敏感的代码段如果能够明确保证对象的生命周期使用原始指针可以避免原子操作的开销。与C API交互许多C库函数接受或返回原始指针。在接口边界处可以从智能指针用.get()获取原始指针传入或者用智能指针接管C API返回的原始指针注意指定正确的删除器如free。记住一个简单的原则所有权清晰时用unique_ptr需要共享所有权时用shared_ptr仅观察时用weak_ptr、原始指针或引用。清晰的所有权设计是编写健壮、易维护C代码的关键。