浪客诺特免安装绿色中文版
1.01G · 2025-09-19
std::shared_ptr
是现代C++资源管理工具箱中另一件至关重要的工具,但其设计哲学和适用场景与 std::unique_ptr
截然不同。
std::unique_ptr
完美解决了独占所有权的问题,但现实世界并非总是独占的。很多场景需要多个实体共享访问同一份资源,且资源的生命周期需要持续到最后一个使用者结束使用为止。
在 std::shared_ptr
出现之前,实现这种“共享所有权”是极其棘手且容易出错的:
delete
资源。这个过程繁琐且极易出错(漏增、漏减)。std::shared_ptr
的核心价值在于:它通过自动化的引用计数技术,安全、高效地实现了资源的共享所有权模型,将开发者从手动管理共享资源生命周期的泥潭中彻底解放出来。
std::shared_ptr
是一个智能指针模板,它对其所指向的对象采用共享所有权(shared ownership) 模型。
“共享”意味着:
shared_ptr
可以同时“拥有”(指向)同一个对象。shared_ptr
被销毁或重置时(即引用计数降为0),对象才会被自动销毁,其内存才会被释放。它通常用于表示“如果还有人在用,它就活着;没人在用了,它就自动消失”的语义。
std::shared_ptr
的实现比 unique_ptr
复杂,因为它需要管理两个实体:
shared_ptr
正拥有该对象。weak_ptr
在观察该对象(稍后解释)。其内存布局通常如下图所示:
// 简化的 std::shared_ptr 实现概念
template<typename T>
class shared_ptr {
private:
T* ptr; // 指向托管对象的指针
ControlBlock* control_block; // 指向控制块的指针
public:
// 构造函数 (通过 std::make_shared 创建是最高效的方式)
template<typename... Args>
explicit shared_ptr(Args&&... args) {
// make_shared 会一次性分配内存,同时存放对象和控制块
control_block = new ControlBlock();
ptr = new (control_block->object_storage) T(std::forward<Args>(args)...);
control_block->use_count = 1;
}
// 拷贝构造函数:共享所有权,引用计数+1
shared_ptr(const shared_ptr& other) noexcept : ptr(other.ptr), control_block(other.control_block) {
if (control_block) {
++control_block->use_count;
}
}
// 析构函数:引用计数-1,若为0则销毁对象和control_block
~shared_ptr() {
if (control_block) {
--control_block->use_count;
if (control_block->use_count == 0) {
// 1. 调用析构函数销毁对象
ptr->~T();
// 2. 如果弱引用计数也为0,则销毁控制块
if (control_block->weak_count == 0) {
delete control_block;
}
}
}
}
// ... 移动构造、赋值运算符等其他成员
};
关键实现要点:
shared_ptr
都指向同一个控制块,这是它们协同工作的基础。std::make_shared
优化:std::make_shared<T>(...)
通常会进行一次单一的内存分配,来同时存储对象本身和控制块。这提高了性能(减少一次分配)和局部性(对象和控制块在一起)。这是强烈推荐的创建方式。std::unique_ptr
将删除器作为模板参数不同,std::shared_ptr
的删除器是控制块的一部分,通过类型擦除技术存储。这意味着两个拥有不同删除器的 shared_ptr<T>
仍然是相同类型,可以放在同一个容器里。#include <memory>
// 1. 创建:始终优先使用 std::make_shared
auto sp1 = std::make_shared<MyClass>(arg1, arg2); // 高效且安全
// 2. 拷贝:共享所有权,引用计数增加
auto sp2 = sp1; // sp1 和 sp2 现在共享同一对象,use_count == 2
// 3. 像普通指针一样使用
sp1->doSomething();
(*sp2).doAnotherThing();
if (sp1) { // 判断是否为空
// ...
}
// 4. 手动放弃所有权 (不会影响引用计数)
sp1.reset(); // sp1 变为空,原对象的 use_count 减为 1
sp2.reset(); // sp2 变为空,use_count 减为 0,对象被销毁
std::weak_ptr
配合解决循环引用这是 shared_ptr
最重要的合作伙伴,用于解决其最著名的陷阱——循环引用(Cyclic Reference)。
问题场景:
struct B;
struct A {
std::shared_ptr<B> b_ptr;
~A() { std::cout << "A destroyedn"; }
};
struct B {
std::shared_ptr<A> a_ptr; // 循环引用!
~B() { std::cout << "B destroyedn"; }
};
void leak() {
auto a = std::make_shared<A>();
auto b = std::make_shared<B>();
a->b_ptr = b; // a.use_count -> 1, b.use_count -> 2
b->a_ptr = a; // a.use_count -> 2, b.use_count -> 2
}
// 函数结束,a 和 b 析构,use_count 都从 2 减为 1。
// 因为引用计数永不为0,A 和 B 的对象永远无法被销毁 -> 内存泄漏。
解决方案:使用 std::weak_ptr
weak_ptr
是一种“弱引用”,它指向一个由 shared_ptr
管理的对象,但不增加其引用计数。
struct B;
struct A {
std::shared_ptr<B> b_ptr;
~A() { std::cout << "A destroyedn"; }
};
struct B {
std::weak_ptr<A> a_ptr; // 将其中之一改为 weak_ptr
~B() { std::cout << "B destroyedn"; }
};
void no_leak() {
auto a = std::make_shared<A>();
auto b = std::make_shared<B>();
a->b_ptr = b; // b.use_count -> 2
b->a_ptr = a; // a.use_count 仍然为 1! (weak_ptr 不增加计数)
}
// 函数结束:
// 1. 'a' 析构,use_count 从 1 减为 0 -> A 对象被销毁。
// 2. 'b' 析构,use_count 从 2 减为 1 -> 但因为 A 对象已死,b->a_ptr 失效,且最终 b.use_count 减为 0 -> B 对象被销毁。
weak_ptr
的使用: 它不能直接访问资源,必须通过 .lock()
方法尝试获取一个临时的 shared_ptr
。
if (auto temp_shared_ptr = weak_ptr.lock()) {
// 获取成功,说明对象还活着,可以安全使用 temp_shared_ptr
temp_shared_ptr->doSomething();
} else {
// 对象已经被释放了
}
std::make_shared
。它更快、更安全(异常安全)、内存利用率更高。std::unique_ptr
,只在确需共享所有权时才使用 std::shared_ptr
。共享所有权是有成本的(控制块、原子操作)。std::weak_ptr
来“打破”循环。shared_ptr
。
MyClass* raw_ptr = new MyClass();
std::shared_ptr<MyClass> sp1(raw_ptr);
std::shared_ptr<MyClass> sp2(raw_ptr); // 灾难!两个独立的控制块会双重删除 raw_ptr。
如果你必须从裸指针构造,请直接在一行代码中完成:
std::shared_ptr<MyClass> sp1(new MyClass()); // OK,但不如 make_shared
shared_ptr
本身作为函数参数,除非函数意图共享所有权(即需要拷贝一份)。如果函数只是需要使用对象,传递裸指针 (ptr.get()
) 或引用即可。不必要的拷贝会增加原子操作的开销。
void func(std::shared_ptr<MyClass> sp)
(值传递)void func(MyClass* ptr)
或 void func(MyClass& ref)
shared_ptr
能保证托管对象是线程安全的。引用计数是线程安全的,但对象本身的 doSomething()
方法是否需要加锁,取决于对象自身的实现。特性 | std::shared_ptr | std::unique_ptr |
---|---|---|
所有权模型 | 共享所有权 | 独占所有权 |
拷贝语义 | 支持(引用计数+1) | 禁止 |
移动语义 | 支持(所有权转移,计数不变) | 支持(所有权转移) |
开销 | 较大(控制块、原子操作) | 极小(几乎为零) |
核心机制 | 引用计数 | 独占性(移动语义) |
首选创建方式 | std::make_shared | std::make_unique |
典型用途 | 共享资源、缓存、观察者模式、复杂关系图 | 独占资源、工厂模式、实现 PImpl 惯用法、函数内部资源管理 |
核心思想:std::shared_ptr
提供了了一种强大而方便的共享资源生命周期管理方式,但“能力越大,责任越大”。你必须清醒地意识到其性能开销和循环引用的陷阱,并学会用 std::weak_ptr
与之配合。在绝大多数情况下,std::unique_ptr
应该是你的默认选择,std::shared_ptr
则是在共享所有权不可避免时的终极解决方案。
C++底层机制推荐阅读
【C++基础知识】深入剖析C和C++在内存分配上的区别
【底层机制】【C++】vector 为什么等到满了才扩容而不是提前扩容?
【底层机制】malloc 在实现时为什么要对大小内存采取不同策略?
【底层机制】剖析 brk 和 sbrk的底层原理
【底层机制】为什么栈的内存分配比堆快?
【底层机制】右值引用是什么?为什么要引入右值引用?
【底层机制】auto 关键字的底层实现机制
【底层机制】std::unordered_map 扩容机制
【底层机制】稀疏文件--是什么、为什么、好在哪、实现机制
【底层机制】【编译器优化】RVO--返回值优化
【基础知识】仿函数与匿名函数对比
【底层机制】【C++】std::move 为什么引入?是什么?怎么实现的?怎么正确用?
【底层机制】emplace_back 为什么引入?是什么?怎么实现的?怎么正确用?
【底层机制】【编译器优化】循环优化--为什么引入?怎么实现的?流程啥样?
【底层机制】std::string 解决的痛点?是什么?怎么实现的?怎么正确用?
【底层机制】std::unique_ptr 解决的痛点?是什么?如何实现?怎么正确使用?
关注公众号,获取更多底层机制/ 算法通俗讲解干货!