1. 项目概述为什么我们需要shared_ptr在C的世界里内存管理一直是开发者必须直面的核心挑战。手动new和delete就像在悬崖边行走稍有不慎就会导致内存泄漏、悬空指针或者重复释放这些Bug往往难以追踪是无数项目崩溃的根源。我自己在早期做图形渲染引擎时就因为一个复杂的对象所有权传递问题导致内存泄漏排查了整整两天才定位到一个不起眼的delete遗漏。正是这种切肤之痛让我深刻理解了智能指针的价值。shared_ptr是C11标准库引入的智能指针之一它实现了基于引用计数的共享所有权模型。简单来说就是多个shared_ptr可以共同“拥有”同一个堆内存对象系统会默默记录有多少个“主人”。只有当最后一个“主人”即shared_ptr离开作用域或被重置时它所管理的对象才会被自动销毁。这极大地简化了资源生命周期管理尤其是在对象需要在多个模块、多个线程间传递和共享的场景下。它特别适合谁呢如果你是从C语言转来对malloc/free感到厌倦或者你正在编写一个中型以上的C项目对象关系复杂亦或是你正在准备面试被问到“如何避免内存泄漏”时shared_ptr都是你必须掌握的核心工具。接下来我会结合近十年的使用和教学经验从最基础的用法到深坑规避为你拆解shared_ptr的方方面面。2. shared_ptr的核心原理与内部机制要玩转一个工具不能只停留在“怎么用”还得明白它“为什么”这么工作。理解shared_ptr的内部机制能帮助你在复杂场景下做出正确判断避免误用。2.1 引用计数共享所有权的基石shared_ptr的核心是一个引用计数器。这个计数器通常与管理的对象一起被分配在另一块独立的堆内存称为控制块中。每一个shared_ptr的拷贝构造或赋值操作都会使这个引用计数加1每一个shared_ptr的析构或重置reset都会使引用计数减1。当计数减为0时控制块会先析构对象然后释放对象内存最后再释放控制块本身的内存。这里有一个关键细节引用计数本身的操作必须是原子的atomic。因为shared_ptr被设计为线程安全的——多个线程同时拷贝或析构指向同一对象的shared_ptr是安全的。这里的“线程安全”指的是控制块引用计数的增减操作是原子的不会导致计数错误。但这不意味着你通过shared_ptr访问其指向的对象是线程安全的如果多个线程同时读写同一个对象你仍然需要额外的同步机制如互斥锁。注意很多人误以为用了shared_ptr就万事大吉对象访问自然线程安全这是大错特错的。shared_ptr只保证管理对象生命周期的指针本身在多线程操作下不会引发资源管理层面的崩溃如重复释放对象内部数据的并发访问安全仍需开发者自己保证。2.2 控制块的结构与开销一个典型的shared_ptr控制块至少包含以下信息指向被管理对象的指针。引用计数use_count记录有多少个shared_ptr实例共享所有权。弱引用计数weak_count记录有多少个weak_ptr观察着该对象这个我们后面会讲。删除器Deleter一个可调用对象用于销毁对象。默认是delete操作符。分配器Allocator用于分配控制块内存通常使用默认分配器。由于存在控制块shared_ptr会带来两方面的开销内存开销除了对象本身还有控制块的内存通常几十字节。性能开销每次拷贝、赋值、析构都需要原子地操作引用计数。在绝大多数应用场景中这点开销是完全可以接受的它为开发带来的安全性和便利性远超其成本。但在极端性能敏感如高频交易核心路径、嵌入式设备或需要创建海量微小对象的场景下你需要权衡是否使用shared_ptr。2.3 与unique_ptr和weak_ptr的定位差异C11提供了三种智能指针定位清晰各司其职unique_ptr独占所有权。一个对象只能由一个unique_ptr拥有不能拷贝只能移动。它开销极小通常无额外开销是默认的资源管理选择。当你明确知道一个资源在任一时刻只有一个所有者时优先使用unique_ptr。shared_ptr共享所有权。用于需要多个部分共享对象生命周期且没有明确单一所有者的场景。weak_ptr弱引用。它不增加引用计数只是“观察”shared_ptr管理的对象用于打破shared_ptr的循环引用。一个常见的误区是滥用shared_ptr。我见过很多新手只要用到指针就上shared_ptr这会导致程序逻辑模糊所有权不清晰和潜在的性能浪费。我的经验法则是默认使用unique_ptr仅在确需共享所有权时使用shared_ptr并用weak_ptr来解决循环引用问题。3. shared_ptr的详细使用教程与核心API解析理论说再多不如上手练。我们来看shared_ptr的具体用法我会把每个关键API的细节和背后的意图讲清楚。3.1 创建shared_ptr的多种方式创建shared_ptr主要有以下几种方式各有适用场景。1. 使用std::make_shared推荐这是C11引入的工厂函数也是创建shared_ptr的首选方式。#include memory #include iostream class MyClass { public: MyClass(int val) : data(val) { std::cout MyClass constructed\n; } ~MyClass() { std::cout MyClass destroyed\n; } void print() { std::cout Data: data std::endl; } private: int data; }; int main() { // 使用make_shared创建 auto sp1 std::make_sharedMyClass(42); // 构造对象引用计数为1 sp1-print(); // 使用-操作符访问成员 auto sp2 sp1; // 拷贝构造引用计数变为2 std::cout sp1 use_count: sp1.use_count() std::endl; // 输出2 return 0; } // sp2和sp1依次析构引用计数归0对象被销毁优点高效。make_shared通常只需一次内存分配同时为对象和控制块分配连续内存减少了内存碎片和分配开销。优点异常安全。如果构造函数参数本身在求值过程中可能抛出异常make_shared能保证不会发生内存泄漏。限制无法指定自定义删除器或分配器。如果对象很大且weak_ptr生命周期长可能导致对象内存无法及时释放因为对象和控制块内存在一起。2. 使用构造函数直接创建// 从原始指针构造不推荐有风险 std::shared_ptrMyClass sp3(new MyClass(100)); // 使用std::allocate_shared指定分配器高级用法 #include memory templatetypename T struct MyAllocator { /* ... 自定义分配器实现 ... */ }; MyAllocatorMyClass myAlloc; auto sp4 std::allocate_sharedMyClass(myAlloc, 200);注意重要陷阱绝对不要使用同一个原始指针初始化多个独立的shared_ptrMyClass* rawPtr new MyClass(5); std::shared_ptrMyClass spA(rawPtr); std::shared_ptrMyClass spB(rawPtr); // 灾难两个独立的控制块会重复释放上面这段代码会导致对象被delete两次引发未定义行为通常是程序崩溃。每个new出来的对象应该只用于初始化一个shared_ptr。如果确实需要从已有指针构造并且担心这个问题可以使用std::enable_shared_from_this后面会讲到。3. 使用reset方法reset()方法用于重置shared_ptr减少原管理对象的引用计数并让其管理新的指针或置空。std::shared_ptrMyClass sp5; sp5.reset(new MyClass(33)); // sp5现在管理新对象 sp5.reset(); // 释放当前对象sp5变为空3.2 关键成员函数与操作use_count()返回当前共享对象的shared_ptr数量。注意此函数通常用于调试因其返回值可能已经过时其他线程可能正在更改计数且性能开销因实现而异不要用于业务逻辑。unique()判断use_count() 1。同样主要用于调试。get()返回存储的原始指针。要非常小心地使用这个函数你绝不能对这个返回的指针进行delete操作也尽量避免用它来创建另一个智能指针。它主要用于需要向遗留C接口传递原始指针的场景。void legacyCApi(MyClass* ptr); auto sp std::make_sharedMyClass(99); legacyCApi(sp.get()); // 可以但需确保legacyCApi不会试图删除ptrswap()交换两个shared_ptr所管理的对象。operator bool()判断是否管理着一个对象即get() ! nullptr。常用于条件判断。std::shared_ptrMyClass sp; if (sp) { // 等价于 if (sp.get() ! nullptr) // 未指向对象不会执行 } sp std::make_sharedMyClass(1); if (sp) { // 现在会执行了 }3.3 自定义删除器Deleter默认情况下shared_ptr使用delete来销毁对象。但如果你管理的是通过malloc分配的内存、文件句柄、网络套接字或者其他需要特殊清理的资源就需要自定义删除器。#include memory #include cstdlib #include iostream void freeInt(int* p) { std::cout Custom deleter: free\n; std::free(p); // 使用free释放malloc分配的内存 } struct FileCloser { void operator()(std::FILE* fp) const { if (fp) { std::cout Closing file\n; std::fclose(fp); } } }; int main() { // 1. 使用函数指针作为删除器 int* mallocedInt (int*)std::malloc(sizeof(int)); *mallocedInt 42; std::shared_ptrint sp1(mallocedInt, freeInt); // 2. 使用Lambda表达式更灵活 std::shared_ptrint sp2( (int*)std::malloc(sizeof(int)), [](int* p) { std::cout Lambda deleter\n; std::free(p); } ); *sp2 43; // 3. 使用函数对象如上面的FileCloser std::shared_ptrstd::FILE sp3(std::fopen(test.txt, r), FileCloser()); // 当sp1, sp2, sp3离开作用域时它们对应的自定义删除器会被调用 return 0; }自定义删除器是shared_ptr强大灵活性的体现它使得shared_ptr可以管理几乎任何类型的资源实现RAII资源获取即初始化范式。3.4 别名构造Aliasing Constructor这是一个高级但非常有用的特性。它允许一个shared_ptr实例与另一个shared_ptr共享控制块即引用计数但指向一个不同的对象通常是原对象的一个成员或关联对象。struct MyStruct { int value 100; std::string name test; }; int main() { auto spStruct std::make_sharedMyStruct(); // 管理整个结构体 // 创建一个“别名”指针它共享spStruct的控制块但指向其成员value std::shared_ptrint spMember(spStruct, spStruct-value); std::cout spStruct.use_count() std::endl; // 输出2 std::cout spMember.use_count() std::endl; // 输出2 // 当spStruct和spMember都销毁后MyStruct对象才会被释放 // 通过spMember可以安全地访问value只要MyStruct对象还活着 std::cout *spMember std::endl; // 输出100 return 0; }这个特性在需要返回对象内部状态的指针但又想保持该状态与对象生命周期一致时非常有用。它避免了手动管理成员指针生命周期的复杂性。4. 循环引用问题与weak_ptr的正确使用这是shared_ptr使用中最经典、也最容易踩坑的问题。循环引用会导致引用计数永远无法归零从而引发内存泄漏。4.1 循环引用是如何发生的看一个典型的父子节点或双向链表例子#include memory #include iostream class BadNode { public: std::shared_ptrBadNode parent; std::shared_ptrBadNode child; ~BadNode() { std::cout BadNode destroyed\n; } }; int main() { auto node1 std::make_sharedBadNode(); auto node2 std::make_sharedBadNode(); node1-child node2; // node1引用node2node2的use_count2 node2-parent node1; // node2引用node1node1的use_count2 // 离开作用域时 // node2析构但node1的child还指向它所以node2的use_count从2减为1对象不释放。 // node1析构但node2的parent还指向它所以node1的use_count从2减为1对象不释放。 // 结果两个对象都无法释放内存泄漏 std::cout End of main\n; return 0; } // 程序输出只有 End of main不会有 BadNode destroyed4.2 使用weak_ptr打破循环weak_ptr是一种不控制对象生命周期的智能指针它指向一个由shared_ptr管理的对象但不会增加该对象的引用计数。你需要将weak_ptr转换为shared_ptr通过lock()方法才能访问对象。class GoodNode { public: std::shared_ptrGoodNode child; std::weak_ptrGoodNode parent; // 关键将parent改为weak_ptr ~GoodNode() { std::cout GoodNode destroyed\n; } }; int main() { auto node1 std::make_sharedGoodNode(); auto node2 std::make_sharedGoodNode(); node1-child node2; node2-parent node1; // node1的use_count仍然是1 // 离开作用域时 // node2析构其use_count从2减为1只有node1-child引用它。 // node1析构其use_count从1减为0因此node1对象被销毁。 // node1销毁导致其成员child即shared_ptrGoodNode析构这使得node2的use_count从1减为0node2对象也被销毁。 std::cout End of main\n; return 0; } // 程序输出 // GoodNode destroyed // GoodNode destroyed // End of mainweak_ptr的常用操作lock()尝试获取一个指向被管理对象的shared_ptr。如果对象还存在即引用计数0则返回一个有效的shared_ptr会增加引用计数如果对象已被释放则返回一个空的shared_ptr。std::weak_ptrGoodNode weakNode node2-parent; if (auto sharedParent weakNode.lock()) { // 安全的访问方式 // 对象还存在可以使用sharedParent std::cout Parent is alive\n; } else { std::cout Parent has been destroyed\n; }expired()检查被观察的对象是否已被释放即引用计数为0。比lock()稍快但存在竞态条件expired()返回false后在调用lock()之前对象可能被其他线程释放。因此通常直接使用lock()是更安全的选择。4.3 设计模式中的典型应用观察者模式与缓存weak_ptr在观察者模式中非常有用。主题Subject持有观察者Observer的weak_ptr列表。当需要通知观察者时遍历列表对每个weak_ptr调用lock()如果返回有效的shared_ptr就通知如果无效观察者已销毁则将其从列表中移除。这样避免了主题持有观察者的强引用导致观察者无法销毁的问题。在实现对象缓存时也可以用shared_ptr管理缓存项对外返回weak_ptr。客户端通过lock()获取对象如果缓存项还在就使用如果已被换出则重新加载。这保证了缓存不会阻止对象的正常生命周期结束。5. 高级主题与性能优化实践当你在大型项目或高性能场景中使用shared_ptr时以下这些高级主题和优化技巧会非常关键。5.1 enable_shared_from_this安全地从this获取shared_ptr考虑这样一个场景一个对象被shared_ptr管理在它的成员函数中你需要传递一个指向自身的shared_ptr给其他函数例如放入一个回调队列。直接使用this指针构造一个新的shared_ptr是危险的因为这会产生一个新的控制块导致重复释放。class BadClass : public std::enable_shared_from_thisBadClass { public: std::shared_ptrBadClass getBadShared() { return std::shared_ptrBadClass(this); // 危险 } }; int main() { auto sp1 std::make_sharedBadClass(); auto sp2 sp1-getBadShared(); // sp2拥有独立控制块 // sp1和sp2析构时都会尝试删除同一个this崩溃 }正确的做法是让类继承自std::enable_shared_from_thisT并使用其shared_from_this()成员函数。class GoodClass : public std::enable_shared_from_thisGoodClass { public: std::shared_ptrGoodClass getGoodShared() { return shared_from_this(); // 安全返回一个共享当前控制块的shared_ptr } // 注意构造函数和析构函数内不能调用shared_from_this() }; int main() { auto sp1 std::make_sharedGoodClass(); auto sp2 sp1-getGoodShared(); // sp2与sp1共享控制块引用计数为2 // 安全 }重要限制shared_from_this()只能在对象已经被一个shared_ptr管理之后调用。也就是说你不能在栈上对象或者通过unique_ptr管理的对象上调用它否则会抛出std::bad_weak_ptr异常。5.2 性能考量与make_shared的优劣如前所述std::make_shared通常是创建shared_ptr的最佳选择因为它将对象和控制块的内存分配合并为一次。但这带来一个潜在的缺点对象内存与控制块生命周期绑定。由于对象和控制块是连续内存只有当所有shared_ptr和所有指向该控制块的weak_ptr都销毁后整块内存才会被释放。这意味着即使所有shared_ptr都已销毁对象理论上可析构但只要还有一个weak_ptr存在对象占用的内存就无法回收只有控制块部分的信息可以复用。这在对象体积非常大且weak_ptr可能长期存在时会导致内存占用高于预期。解决方案如果对象很小或者weak_ptr生命周期很短用make_shared。如果对象很大例如一个巨大的缓冲区且可能存在长期存活的weak_ptr可以考虑使用new和构造函数让对象内存独立分配以便及时释放。std::shared_ptrHugeObject sp(new HugeObject(/*...*/), customDeleter); // 或者使用std::allocate_shared配合自定义分配器进行更精细的控制。5.3 多线程安全使用指南再次强调shared_ptr的线程安全模型线程安全多个线程同时读写不同的shared_ptr实例是安全的。多个线程同时拷贝/赋值/重置同一个shared_ptr实例是安全的引用计数原子操作。非线程安全多个线程通过不同的shared_ptr实例但指向同一对象访问和修改对象本身是非线程安全的需要额外的同步。一个常见的多线程使用模式是将shared_ptr通过值传递的方式传递给线程。这会在新线程中创建一个新的shared_ptr实例增加引用计数从而保证对象在线程执行期间存活。void worker(std::shared_ptrMyClass obj) { // 在这个线程中obj是一个独立的shared_ptr实例它保证了MyClass对象存活 // 但访问obj-data需要加锁如果其他线程也在修改的话 } int main() { auto globalObj std::make_sharedMyClass(); std::thread t(worker, globalObj); // 通过值传递引用计数1 t.detach(); // main函数可能结束但globalObj对象会等到worker线程中的obj析构后才可能释放 }5.4 与标准库容器及算法的配合shared_ptr可以像普通指针一样用于标准库容器如std::vectorstd::shared_ptrMyClass。这非常有用例如用于实现对象池或管理多态对象的集合。std::vectorstd::shared_ptrBase polymorphicCollection; polymorphicCollection.push_back(std::make_sharedDerived1()); polymorphicCollection.push_back(std::make_sharedDerived2()); for (const auto ptr : polymorphicCollection) { ptr-virtualFunction(); // 正确调用派生类的函数 } // 当vector被清空或销毁时所有对象会自动释放使用算法时需要注意比较的是指针值还是对象值。默认情况下比较的是shared_ptr本身即它们是否指向同一地址。如果你想根据对象的值来排序或查找需要提供自定义的比较谓词。std::vectorstd::shared_ptrMyClass vec; // ... 填充vec // 按指针地址排序无意义 std::sort(vec.begin(), vec.end()); // 按对象内部的value成员排序 std::sort(vec.begin(), vec.end(), [](const std::shared_ptrMyClass a, const std::shared_ptrMyClass b) { return a-value b-value; });6. 常见陷阱、调试技巧与最佳实践总结即使理解了所有原理实际编码中还是会遇到各种坑。这里总结一些我踩过的坑和调试经验。6.1 典型陷阱与错误用法不要使用同一个原始指针初始化多个独立的shared_ptr前文已强调这是导致重复释放的最常见原因。避免循环引用在可能存在环形引用如双向链表、树结构中父节点引用子节点同时子节点引用父节点、观察者模式中相互持有强引用的地方审慎地将某一方向改为weak_ptr。慎用get()返回的指针绝不delete它也避免用它来创建新的智能指针或延长其生命周期。它的生命周期完全由shared_ptr控制。注意enable_shared_from_this的使用时机确保对象已被shared_ptr管理后再调用shared_from_this()。性能不是万能的借口在非性能瓶颈处优先考虑代码的安全性和清晰度。shared_ptr的额外开销在大多数场景下微不足道。对象析构顺序由于shared_ptr的析构会触发对象析构当多个shared_ptr管理的对象相互依赖时例如对象A的析构函数中需要访问对象B你需要确保依赖关系不会形成“析构顺序死锁”。通常通过weak_ptr来访问可能已析构的依赖对象是更安全的方式。6.2 调试内存问题尽管shared_ptr能解决大部分内存泄漏但循环引用导致的泄漏依然会发生。以下是一些调试手段使用use_count()进行调试在怀疑有泄漏的地方打印shared_ptr的use_count()。如果其值在预期应该为0的时候仍然大于0就可能存在循环引用或意外的强引用持有。auto obj std::make_sharedMyClass(); std::cout count after creation: obj.use_count() std::endl; // 1 auto anotherRef obj; std::cout count after copy: obj.use_count() std::endl; // 2利用Valgrind、AddressSanitizer等工具这些内存检查工具可以帮你发现更隐蔽的内存问题。在自定义删除器中添加日志在对象的删除器中打印日志可以明确知道对象是否被正确销毁。auto sp std::shared_ptrMyClass(new MyClass, [](MyClass* p) { std::cout Deleting MyClass at p std::endl; delete p; });6.3 最佳实践清单根据多年经验我总结了以下使用shared_ptr的最佳实践遵循它们能让你避开绝大多数坑首选std::make_shared除非你需要自定义删除器、分配器或者有明确的大对象与weak_ptr长期共存的问题。明确所有权语义在设计中就想清楚一个对象是应该被独占unique_ptr还是共享shared_ptr。模糊的所有权是许多问题的根源。将shared_ptr按值传递函数如果需要共享所有权应该按值传递shared_ptr参数。这明确表达了所有权的共享并自动处理引用计数。如果函数只是使用对象而不需要共享所有权则传递原始指针ptr.get()或引用。使用weak_ptr打破循环在设计类关系时如果发现可能形成环立即考虑将其中一个引用改为weak_ptr。避免从成员函数返回shared_ptr除非是enable_shared_from_this的场景否则这通常会暴露内部实现并可能无意中延长对象生命周期。优先返回原始指针或引用。对于数组使用std::shared_ptrT[]C17或自定义删除器shared_ptr默认使用delete p对于数组应使用delete[] p。C17提供了shared_ptrT[]的特化版本。在C17之前需要提供自定义删除器。// C17 之前 std::shared_ptrint sp(new int[10], std::default_deleteint[]()); // 或者使用lambda std::shared_ptrint sp(new int[10], [](int* p) { delete[] p; }); // C17 及之后 std::shared_ptrint[] sp(new int[10]);在性能关键路径上考虑传递原始指针或引用如果你能百分百确定对象的生命周期在调用期间是有效的为了消除引用计数的原子操作开销可以在最内层循环传递get()得到的指针。最后记住智能指针是工具目的是让资源管理更简单、更安全而不是让你的设计变得更复杂。从unique_ptr开始只在必要时引入shared_ptr和weak_ptr你的C代码会变得更加健壮和清晰。在实际项目中我通常会先用手绘图理清对象间的所有权关系再决定使用哪种智能指针这个方法帮我避免了许多设计初期的错误。