文章目录
1. shared_ptr 的基本概念
shared_ptr
是 C++11 引入的智能指针,用于实现共享所有权(shared ownership)的内存管理。多个 shared_ptr
可以同时拥有同一个对象,当最后一个 shared_ptr
被销毁时,对象才会被自动删除。
核心特性
- 共享所有权:多个指针可以共享同一对象
- 引用计数:内部维护引用计数器
- 自动释放:当引用计数归零时自动释放资源
- 线程安全:引用计数操作是原子的(但对象访问需要额外同步)
2. shared_ptr 的核心实现原理
2.1 基本数据结构
shared_ptr
通常包含两个指针:
- 指向管理对象的指针(通常称为存储指针)
- 指向控制块的指针(包含引用计数等元数据)
template<typename T>
class shared_ptr {
private:
T* ptr; // 指向管理对象
ControlBlock* control; // 指向控制块
};
2.2 控制块结构
控制块通常包含:
struct ControlBlock {
std::atomic<size_t> shared_count; // 共享引用计数
std::atomic<size_t> weak_count; // weak_ptr 计数
Deleter deleter; // 删除器
Allocator allocator; // 分配器
// 可能还有其他元数据
};
2.3 引用计数机制
引用计数变化规则:
- 构造时:引用计数置为1
- 拷贝构造:引用计数+1
- 赋值操作:原指针引用计数-1,新指针引用计数+1
- 析构时:引用计数-1,若归零则删除对象
2.4 内存管理流程示例
{
// 创建第一个 shared_ptr
std::shared_ptr<int> p1(new int(42));
// 控制块: shared_count=1, weak_count=0
{
// 拷贝构造
std::shared_ptr<int> p2 = p1;
// 控制块: shared_count=2, weak_count=0
}
// p2 析构
// 控制块: shared_count=1, weak_count=0
// 创建 weak_ptr
std::weak_ptr<int> wp = p1;
// 控制块: shared_count=1, weak_count=1
}
// p1 析构
// 控制块: shared_count=0, weak_count=1
// 对象被删除,但控制块保留(直到 weak_count=0)
3. shared_ptr 的线程安全性分析
3.1 官方标准规定
根据 C++ 标准(§20.8.2.6):
- 引用计数操作是原子的,线程安全
- 管理的对象访问不是线程安全的,需要外部同步
- 控制块本身是线程安全的,但不同 shared_ptr 实例间的操作需要同步
3.2 具体线程安全场景
操作场景 | 线程安全 | 说明 |
---|---|---|
多个线程同时拷贝同一个 shared_ptr | ✅ 安全 | 引用计数原子操作 |
多个线程同时析构 shared_ptr | ✅ 安全 | 引用计数原子操作 |
多个线程通过不同 shared_ptr 访问同一对象 | ❌ 不安全 | 需要外部同步 |
一个线程修改 shared_ptr 另一个线程读取 | ❌ 不安全 | 需要同步 |
使用 weak_ptr.lock() 升级为 shared_ptr | ✅ 安全 | 原子操作 |
3.3 线程安全示例
安全操作:
std::shared_ptr<int> globalPtr = std::make_shared<int>(42);
// 线程1
auto p1 = globalPtr; // 安全,引用计数原子增加
// 线程2
auto p2 = globalPtr; // 安全,引用计数原子增加
不安全操作:
// 线程1
*globalPtr = 10; // 不安全,对象访问需要同步
// 线程2
int val = *globalPtr; // 不安全,竞态条件
安全访问方案:
std::mutex mtx;
// 线程1
{
std::lock_guard<std::mutex> lock(mtx);
*globalPtr = 10;
}
// 线程2
{
std::lock_guard<std::mutex> lock(mtx);
int val = *globalPtr;
}
4. shared_ptr 的高级特性
4.1 自定义删除器
auto FileDeleter = [](FILE* fp) {
if(fp) fclose(fp);
};
std::shared_ptr<FILE> filePtr(fopen("test.txt", "r"), FileDeleter);
4.2 别名构造(Aliasing Constructor)
允许一个 shared_ptr 共享另一个 shared_ptr 的所有权,但指向不同的对象:
struct Node {
int value;
std::shared_ptr<Node> next;
};
auto head = std::make_shared<Node>();
auto valuePtr = std::shared_ptr<int>(head, &head->value);
// valuePtr 共享 head 的所有权,但指向 head->value
4.3 make_shared 的优势
推荐使用 make_shared
而非直接 new
:
auto p = std::make_shared<MyClass>(args...);
优势:
- 更高性能:单次内存分配(对象+控制块)
- 异常安全:避免内存泄漏
- 代码简洁:不需要显式 new
5. shared_ptr 的潜在问题与解决方案
5.1 循环引用问题
struct A {
std::shared_ptr<B> b;
};
struct B {
std::shared_ptr<A> a;
};
auto a = std::make_shared<A>();
auto b = std::make_shared<B>();
a->b = b;
b->a = a; // 循环引用,内存泄漏
解决方案:使用 weak_ptr
打破循环
struct B {
std::weak_ptr<A> a; // 改为 weak_ptr
};
5.2 性能开销
相比 unique_ptr
,shared_ptr
有额外开销:
- 内存开销:控制块(通常16-24字节)
- 性能开销:原子操作引用计数
5.3 避免从裸指针多次构造
int* raw = new int(10);
std::shared_ptr<int> p1(raw);
std::shared_ptr<int> p2(raw); // 灾难性错误:双重释放
正确做法:
auto p1 = std::make_shared<int>(10);
auto p2 = p1; // 安全共享
6. shared_ptr 与 weak_ptr 的配合
weak_ptr
是 shared_ptr
的配套智能指针,它:
- 不增加引用计数
- 不影响对象生命周期
- 可通过
lock()
升级为shared_ptr
auto shared = std::make_shared<int>(42);
std::weak_ptr<int> weak = shared;
if(auto temp = weak.lock()) { // 尝试升级
// 使用 temp
} else {
// 对象已释放
}
7. 性能优化建议
- 优先使用 make_shared:减少内存分配次数
- 避免不必要的 shared_ptr 拷贝:传引用而非值
- 考虑使用 weak_ptr 观察:不参与所有权时
- 评估是否需要共享所有权:能用 unique_ptr 时优先使用
8. 总结
shared_ptr 原理总结:
- 共享所有权:通过引用计数实现多指针共享对象
- 控制块管理:包含引用计数、删除器等元数据
- 原子操作:保证引用计数线程安全
- 自动释放:引用计数归零时调用删除器
线程安全性总结:
- ✅ 安全:引用计数操作(构造、拷贝、析构)
- ❌ 不安全:被管理对象的访问
- 🔄 需要同步:多个线程通过不同 shared_ptr 实例操作同一对象
使用建议:
- 明确需要共享所有权时才使用 shared_ptr
- 多线程访问被管理对象时需要额外同步
- 注意避免循环引用
- 优先使用 make_shared 创建对象
shared_ptr
是 C++ 中最复杂的智能指针,正确理解其原理和线程安全特性对于编写健壮的多线程程序至关重要。