C 中 shared_ptr 详解原理与线程安全性分析一、引言共享所有权的智能指针std::shared_ptr是 C11 引入的智能指针它实现了共享所有权(Shared Ownership)语义——多个shared_ptr可以同时拥有同一个对象通过引用计数(Reference Counting)来追踪所有者的数量。当最后一个shared_ptr被销毁时管理的对象会被自动释放。与unique_ptr的独占所有权不同shared_ptr适用于需要多个所有者共享同一对象的场景。但共享所有权带来了额外的复杂性引用计数的存储、原子操作的开销、以及常见的循环引用问题(需要weak_ptr打破)。二、核心概念速览| 维度 | 说明 ||------|------|| 所有权模型 | 共享(多个 shared_ptr 可拥有同一对象) || 引用计数 | 追踪当前有多少个 shared_ptr 共享同一对象 || 拷贝语义 | 深拷贝(增加引用计数) || 移动语义 | 支持(转移所有权不改变引用计数) || 对象释放时机 | 最后一个 shared_ptr 销毁时(引用计数归零) || 线程安全性 | 引用计数操作是线程安全的但被管理对象本身不是 || 循环引用 | 导致内存泄漏需用 weak_ptr 打破 || 内存开销 | 通常为裸指针的 2 倍(对象指针 控制块指针) || 性能开销 | 拷贝/析构涉及原子引用计数操作 |三、shared_ptr 的内部结构3.1 控制块和对象shared_ptr内部包含两个指针对象指针指向被管理的对象控制块指针指向控制块(Control Block)包含引用计数、弱引用计数、删除器等信息控制块(0x2000)被管理对象(0x1000)shared_ptr2shared_ptr1对象指针: 0x1000控制块指针: 0x2000对象指针: 0x1000控制块指针: 0x2000T 对象shared_count: 2(强引用计数)weak_count: 1(弱引用计数)删除器分配器3.2 简化实现templatetypename T class shared_ptr { private: T* ptr_; // 指向被管理的对象 control_block* cb_; // 指向控制块 struct control_block { size_t shared_count; // 强引用计数 size_t weak_count; // 弱引用计数 // 删除器和分配器... }; public: // 构造函数 explicit shared_ptr(T* p) : ptr_(p), cb_(new control_block{1, 0}) { } // 拷贝构造函数 shared_ptr(const shared_ptr other) : ptr_(other.ptr_), cb_(other.cb_) { cb_-shared_count; // 增加引用计数 } // 析构函数 ~shared_ptr() { --cb_-shared_count; if (cb_-shared_count 0) { delete ptr_; // 删除被管理对象 if (cb_-weak_count 0) { delete cb_; // 删除控制块 } } } // 拷贝赋值运算符 shared_ptr operator(const shared_ptr other) { if (this ! other) { // 先减少当前对象的引用计数 if (--cb_-shared_count 0) { delete ptr_; if (cb_-weak_count 0) delete cb_; } // 再增加新对象的引用计数 ptr_ other.ptr_; cb_ other.cb_; cb_-shared_count; } return *this; } T* get() const { return ptr_; } T* operator-() const { return ptr_; } T operator*() const { return *ptr_; } size_t use_count() const { return cb_-shared_count; } };四、引用计数的生命周期对象控制块(引用计数)shared_ptr bshared_ptr a代码对象控制块(引用计数)shared_ptr bshared_ptr a代码a 和 b 共享同一对象a 析构但不释放对象auto a make_sharedT()shared_count 1创建对象auto b a (拷贝)shared_count 2a 离开作用域shared_count 1b 离开作用域shared_count 0delete 对象控制块也被释放(若无 weak_ptr)五、线程安全性分析5.1 引用计数的线程安全shared_ptr的引用计数操作是线程安全的。C 标准要求shared_ptr的引用计数使用原子操作因此多个线程同时拷贝/销毁指向同一对象的shared_ptr不会导致引用计数损坏。#include memory #include thread #include vector #include iostream int main() { auto sp std::make_sharedint(42); std::vectorstd::thread threads; for (int i 0; i 100; i) { threads.emplace_back([sp]() { // 按值捕获增加引用计数 // sp 的拷贝/析构是线程安全的 auto local sp; // 引用计数原子递增 // local 析构时引用计数原子递减 }); } for (auto t : threads) { t.join(); } std::cout Final use_count: sp.use_count() std::endl; // 1 // 引用计数正确没有被破坏 }5.2 被管理对象的线程安全重要区分引用计数的线程安全不等于被管理对象的线程安全。#include memory #include thread #include iostream class Counter { public: int value_ 0; void increment() { value_; } // 不是线程安全的 }; int main() { auto counter std::make_sharedCounter(); std::vectorstd::thread threads; for (int i 0; i 1000; i) { threads.emplace_back([counter]() { for (int j 0; j 1000; j) { counter-increment(); // ❌ 数据竞争 // shared_ptr 的引用计数是安全的 // 但 Counter::value_ 的修改不是线程安全的 } }); } for (auto t : threads) t.join(); std::cout Final value: counter-value_ std::endl; // 期望 1,000,000实际结果不确定(数据竞争) }正确做法对共享对象本身加锁保护或使用原子操作。#include atomic class ThreadSafeCounter { std::atomicint value_{0}; public: void increment() { value_; } // 原子操作线程安全 int get() const { return value_.load(); } }; // 或使用互斥锁 class MutexCounter { int value_ 0; mutable std::mutex mtx_; public: void increment() { std::lock_guardstd::mutex lock(mtx_); value_; } };5.3 线程安全性总结| 操作 | 线程安全性 | 说明 ||------|:---:|------|| 拷贝 shared_ptr | 安全 | 引用计数原子递增 || 析构 shared_ptr | 安全 | 引用计数原子递减 || 移动 shared_ptr | 安全 | 不涉及引用计数修改 || 修改被管理对象 |不安全| 需要额外的同步机制 || use_count() | 安全(但值可能过期) | 返回值是近似值 || reset() | 安全 | 原子地替换管理的对象 |六、make_shared 与直接构造的差异6.1 内存布局对比// 方式一直接构造(两次内存分配) std::shared_ptrint sp1(new int(42)); // 1. new int(42) — 分配 int 对象 // 2. new control_block — 分配控制块 // 总共 2 次内存分配 // 方式二make_shared(一次内存分配) auto sp2 std::make_sharedint(42); // 1. 分配一块连续内存包含 int 对象和控制块 // 总共 1 次内存分配make_shared(单块内存)连续内存块[控制块 | int 对象]shared_ptrint 对象部分控制块部分直接构造(newintnewcontrol_block)控制块shared_count: 1shared_ptrint 对象6.2 make_shared 的优势与劣势| 维度 | make_shared | 直接构造 ||------|:---:|:---:|| 内存分配次数 | 1 次 | 2 次 || 缓存局部性 | 好(对象和控制块相邻) | 差 || 异常安全 | 是 | 可能有风险 || 自定义删除器 | 不支持 | 支持 || 内存释放时机 | 可能延迟(弱引用存在时) | 对象及时释放 |make_shared 的一个陷阱如果有weak_ptr存在即使所有shared_ptr都销毁了整个内存块(包括对象)也不会释放直到所有weak_ptr也销毁。auto sp std::make_sharedint(42); std::weak_ptrint wp sp; sp.reset(); // shared_count 归零对象析构 // 但内存块没有被释放因为 weak_count 0 // 需要等到 wp 也销毁才能释放内存七、enable_shared_from_this#include memory #include iostream // 错误做法从 this 创建多个 shared_ptr class BadExample { public: std::shared_ptrBadExample getShared() { return std::shared_ptrBadExample(this); // ❌ 危险 // 每次调用都创建新的控制块 // 导致同一对象有多个独立的引用计数 } }; // 正确做法继承 enable_shared_from_this class GoodExample : public std::enable_shared_from_thisGoodExample { public: std::shared_ptrGoodExample getShared() { return shared_from_this(); // 安全共享同一个控制块 } static std::shared_ptrGoodExample create() { return std::shared_ptrGoodExample(new GoodExample()); } private: GoodExample() default; // 私有构造函数强制使用 create() }; int main() { auto obj GoodExample::create(); auto obj2 obj-getShared(); // 正确共享同一个控制块 std::cout use_count: obj.use_count() std::endl; // 2 }八、循环引用与 weak_ptr#include memory #include iostream struct B; struct A { std::shared_ptrB bPtr; ~A() { std::cout A destroyed std::endl; } }; struct B { std::shared_ptrA aPtr; // 导致循环引用 // 应该使用std::weak_ptrA aPtr; ~B() { std::cout B destroyed std::endl; } }; int main() { auto a std::make_sharedA(); auto b std::make_sharedB(); a-bPtr b; b-aPtr a; // 循环引用 // a 和 b 离开作用域 // 引用计数都不为 0 → 内存泄漏 // 不会输出 A destroyed 或 B destroyed }循环引用A::bPtrB::aPtr离开作用域引用计数-1剩余:1离开作用域引用计数-1剩余:1shared_ptrA引用计数: 2(main 中的 a B::aPtr)shared_ptrB引用计数: 2(main 中的 b A::bPtr)main 中的 amain 中的 b两者引用计数都不为 0永远无法销毁解决方案使用weak_ptr打破循环struct B { std::weak_ptrA aPtr; // weak_ptr 不增加引用计数 ~B() { std::cout B destroyed std::endl; } };九、总结std::shared_ptr是 C 中实现共享所有权的智能指针其核心原理和注意事项如下内部结构shared_ptr包含对象指针和控制块指针。控制块存储引用计数(shared_count)、弱引用计数(weak_count)、删除器和分配器。多个shared_ptr共享同一控制块。引用计数机制拷贝构造/赋值时原子递增引用计数析构时原子递减引用计数。当引用计数归零时删除被管理对象当弱引用计数也归零时删除控制块。线程安全性引用计数操作是线程安全的(使用原子操作)被管理对象的访问不是线程安全的需要额外的同步机制多个线程同时拷贝/销毁指向同一对象的shared_ptr是安全的多个线程同时修改被管理对象的状态是不安全的最佳实践优先使用std::make_sharedT()创建shared_ptr使用std::weak_ptr打破循环引用需要从this获取shared_ptr时继承std::enable_shared_from_this不要混淆引用计数的线程安全与被管理对象的线程安全如果不需要共享所有权优先使用std::unique_ptr(更轻量)shared_ptr是 C 中功能最强大的智能指针但也带来了最大的开销。理解其内部机制和线程安全性边界才能在正确的场景中安全、高效地使用它。