在 C++ 中,手动管理资源(new/delete
)容易出错,导致:
•
内存泄漏
•
重复释放
•
异常安全问题
为了解决这些问题,C++11 引入了 智能指针:
•
std::unique_ptr
:独占所有权
•
std::shared_ptr
:共享所有权,通过引用计数管理生命周期
我们从原理、性能和应用场景三个维度来解析。
1
unique_ptr
•
目标:独占资源,不可复制,只能移动(move)
•
使用场景:资源唯一所有权,RAII 风格自动释放
•
核心问题:
•
避免内存泄漏
•
支持移动语义
1
shared_ptr
•
目标:多方共享资源,引用计数控制生命周期
•
使用场景:多线程共享对象、缓存管理
•
核心问题:
•
自动管理生命周期
•
处理多线程环境下的引用计数(atomic)
•
避免循环引用问题
template<typename T, typename Deleter = std::default_delete<T>>
class unique_ptr {
T* ptr; // 原始指针
Deleter deleter; // 删除器
public:
explicit unique_ptr(T* p = nullptr) noexcept : ptr(p) {}
~unique_ptr() { if(ptr) deleter(ptr); }
// 不可拷贝
unique_ptr(const unique_ptr&) = delete;
unique_ptr& operator=(const unique_ptr&) = delete;
// 可移动
unique_ptr(unique_ptr&& u) noexcept : ptr(u.ptr) { u.ptr = nullptr; }
unique_ptr& operator=(unique_ptr&& u) noexcept {
if(this != &u) {
reset(u.ptr);
u.ptr = nullptr;
}
return *this;
}
T* get() const noexcept { return ptr; }
T& operator*() const noexcept { return *ptr; }
T* operator->() const noexcept { return ptr; }
void reset(T* p = nullptr) {
if(ptr) deleter(ptr);
ptr = p;
}
};
•
独占所有权:唯一指针拥有资源,拷贝被禁用
•
移动语义:通过移动实现资源转移
•
RAII:生命周期结束自动释放
•
内存占用小(只存储原始指针 + 删除器)
•
无额外引用计数开销
•
编译期开销低
•
异常安全,避免泄漏
shared_ptr 核心在于 引用计数 和 控制块(control block)。
shared_ptr<T>
├── T* ptr // 实际对象指针
└── ControlBlock* cb // 引用计数和删除器
控制块结构示例:
struct ControlBlock {
std::atomic<size_t> use_count; // 强引用计数
std::atomic<size_t> weak_count; // 弱引用计数
Deleter deleter; // 删除器
T* ptr; // 指向对象
};
•
构造 shared_ptr:初始化 use_count = 1
•
拷贝 shared_ptr:use_count++
(原子操作)
•
析构 shared_ptr:use_count--
,若为 0 调用删除器销毁对象
•
weak_ptr:不增加 use_count
,仅增加 weak_count
,用于解决循环引用
•
线程安全:std::atomic
保证引用计数在多线程下正确
•
控制块分离:
•
对象指针和控制块分离,可以支持 make_shared
内联分配(减少内存碎片)
•
循环引用问题:
•
两个 shared_ptr 互相引用会造成引用计数不为 0
•
需要 weak_ptr 解决
特性 | unique_ptr | shared_ptr |
---|---|---|
内存占用 | 小(指针+删除器) | 控制块额外开销,atomic计数 |
拷贝/移动开销 | 禁止拷贝,移动开销小 | 拷贝需要原子操作,移动开销小 |
多线程安全 | 依赖外部保护 | 引用计数原子操作保证安全 |
循环引用风险 | 无 | 有,需要 weak_ptr |
适用场景 | 独占资源,RAII | 多方共享对象管理,缓存,异步任务 |
1
unique_ptr
•
Ceph 的对象缓存(ObjectCacher)中,每个缓存条目独占内存块
•
TiDB Executor 局部临时对象管理
1
shared_ptr
•
Ceph 多线程 I/O 回调对象共享
•
TiDB RPC/异步任务的共享上下文(Context)
核心思想:能用 unique_ptr 就用 unique_ptr,能静态分析生命周期,避免 atomic 开销;需要多方共享才用 shared_ptr,并结合 weak_ptr 避免循环引用。
•
unique_ptr
•
独占资源,低开销,RAII 自动释放
•
通过 移动语义实现所有权转移
•
shared_ptr
•
共享资源,通过 引用计数 + 控制块管理生命周期
•
支持多线程,但有性能开销
•
循环引用需要 weak_ptr 解决
•
在分布式系统
•
hotspot 对象尽量用 unique_ptr
•
异步回调、共享上下文用 shared_ptr
一句话总结: “能唯一所有权就用 unique_ptr,必须共享就用 shared_ptr,控制引用计数就是控制你的性能。”