C++:unique_ptr、shared_ptr、weak_ptr 有什么区别?
reference counting 怎么实现?
先弄清楚:每个概念是什么
- Ownership 与 RAII
- ownership 就是谁负责 delete。smart pointer 析构时自动释放所管的 object,正常 return 或抛 exception 都不会漏。
- Control block
- shared_ptr 另外管着的一小块内存,记 use count(几个 shared_ptr 在管)、weak count(几个 weak_ptr 在看)和 deleter。use count 到 0 → delete object;weak count 也到 0 → 释放 control block。例:
a、b = a、c = a→ use_count 是 3;a.reset()后是 2。 - Atomic reference count
- 计数用 atomic 加减,所以不同线程各自 copy、销毁指向同一 object 的 shared_ptr 不用加锁。object 本身,以及多个线程写同一个 shared_ptr 变量,都要自己同步。
- make_shared
- 一次 allocation 同时放 object 和 control block,
shared_ptr<T>(new T)至少两次。 - Reference cycle 与 weak_ptr
- A、B 互相用 shared_ptr 指着,外面的 shared_ptr 都没了,两个 use count 还各是 1,永远不 delete。把一边换成 weak_ptr,它不增加 use count,用的时候
lock()拿到 shared_ptr,object 已销毁就拿到空的。
看图:control block 里的两个计数
w.lock() 返回空的。shared_ptr<int>(new int(1)) new 两次,make_shared 一次 new 出一块放两样。下面是 reference cycle,A、B 互相拥有,没人能把 use count 减到 0。child 改用 weak_ptr 指回 parent 后,parent 先到 0 被 delete,child 跟着到 0。30 秒回答
unique_ptr is exclusive ownership: move-only, and with the default deleter it's the size of a raw pointer. shared_ptr is shared ownership: it holds the object pointer and a pointer to a control block with the use count, the weak count and the deleter. Copying increments the use count, destroying or resetting decrements it, and whoever brings it to zero deletes the object. The counts are atomic, so copying shared_ptrs across threads is safe, but the pointee is not protected. make_shared puts the object and control block in one allocation. weak_ptr observes without owning; lock() returns a shared_ptr or an empty one, and it's how you break reference cycles.
三种 smart pointer 对比
| ownership | copy / move | 大小(x86-64) | 什么时候用 | |
|---|---|---|---|---|
unique_ptr | 独占 | 只能 move | 8 B,同 raw pointer | 默认首选 |
shared_ptr | 共享 | copy 时 use count +1 | 16 B + control block | 多个 owner |
weak_ptr | 不拥有 | 不改 use count | 16 B | 观察者、cache、打破 cycle |
手撕代码:最小 shared_ptr
#include <atomic>
#include <utility>
template <class T>
class SharedPtr {
T* p_ = nullptr;
std::atomic<long>* cnt_ = nullptr; // 简化的 control block
void release() {
if (cnt_ && cnt_->fetch_sub(1) == 1) { // 我是最后一个
delete p_;
delete cnt_;
}
}
public:
explicit SharedPtr(T* p = nullptr)
: p_(p), cnt_(p ? new std::atomic<long>(1) : nullptr) {}
SharedPtr(const SharedPtr& o) : p_(o.p_), cnt_(o.cnt_) {
if (cnt_) cnt_->fetch_add(1); // copy:+1
}
SharedPtr(SharedPtr&& o) noexcept : p_(o.p_), cnt_(o.cnt_) {
o.p_ = nullptr; // move:接管,计数不变
o.cnt_ = nullptr;
}
// copy-and-swap:传值时已 +1,旧值随 o 析构 −1;自赋值也安全
SharedPtr& operator=(SharedPtr o) noexcept {
std::swap(p_, o.p_);
std::swap(cnt_, o.cnt_);
return *this;
}
~SharedPtr() { release(); }
T& operator*() const { return *p_; }
T* operator->() const { return p_; }
long use_count() const { return cnt_ ? cnt_->load() : 0; }
};
必答点
unique_ptr独占、只能 move、没有额外开销;shared_ptr共享,control block 存计数;weak_ptr不拥有- copy +1,析构或 reset −1,减到 0 的那个负责 delete object
- 计数是 atomic 的:跨线程 copy 安全,object 本身不安全
- 优先
make_unique/make_shared;cycle 用weak_ptr打破 - 手写:copy assignment 先 +1 再 −1(copy-and-swap),自赋值才安全;move 不改计数
追问
为什么 +1 可以 relaxed,−1 要更强的 memory order?
减到 0 的线程要 delete object,必须先看到其他线程在各自 −1 之前对 object 的所有写,所以 −1 要 release / acquire 语义。+1 不决定谁来 delete,relaxed 就够,手写版用的默认 seq_cst 正确但稍慢。
make_shared 有什么缺点?
只要还有 weak_ptr,control block 就不能释放,同一块里 object 占的内存也回收不了。不能自定义 deleter,constructor 要是 public 的。
同一个 raw pointer 交给两个 shared_ptr 会怎样?
两个 control block 各自计数,各 delete 一次,是 UB(double free)。要共享只能 copy shared_ptr,object 想拿到管理自己的那个就继承 enable_shared_from_this。
手写版和 std::shared_ptr 比还缺什么?
weak count 和 weak_ptr、custom deleter、单次 allocation、Derived → Base 转换。new 计数器抛 exception 时还要先 delete p。
来源cppreference: std::unique_ptr · cppreference: std::shared_ptr(thread safety、Implementation notes)· shared_ptr constructor(Exceptions)· cppreference: std::weak_ptr · weak_ptr::lock · cppreference: std::make_shared(Notes)· cppreference: enable_shared_from_this