Skip to content

手写智能指针(unique_ptr / shared_ptr / weak_ptr) ​

手写 shared_ptr 是 C++ 面试的压轴题:考察 RAII、拷贝/移动语义、原子引用计数、 删除器的类型擦除与访问语义。原理与用法见 智能指针专题。 两档指的是 shared_ptr:4.1 精简版(堆上独立控制块,先交)与 4.2 完整版(类型擦除 + 同块分配 + weak_ptr); unique_ptr 作为前置的独占版本单列在第三节。

一、考察点与澄清 ​

常见问法:手写 unique_ptr(含删除器)、手写 shared_ptr(含控制块)、补 weak_ptr::lock。 动手前需澄清:

  1. 是否需要 weak_ptr(决定控制块内一个计数还是两个,是本题最大的分支);
  2. 是否支持自定义删除器(unique_ptr 的删除器是类型的一部分;shared_ptr 的存在控制块中);
  3. 计数是否需线程安全(是则用 std::atomic;否则可从普通整数起讲到原子演进)。

二、接口与约束 ​

  • 接口
    • unique_ptr:reset / release / get / operator* / operator-> / operator bool、移动构造与移动赋值
    • shared_ptr:拷贝与移动构造/赋值、reset / swap / get / use_count / operator* / operator-> / operator bool
    • weak_ptr:lock / expired、拷贝构造与赋值
  • 不变量
    • 强引用计数等于指向该控制块的 shared_ptr 数量;
    • 弱引用计数等于 weak_ptr 数量加 1(控制块自持一份,故不会在最后一个强引用释放时被提前销毁)。
  • 生命周期:对象在强计数归零时销毁;控制块在强、弱计数均归零时销毁,二者是独立事件
  • 线程安全:控制块的计数增减为原子操作;被管理对象本身不因此获得线程安全
  • 异常安全:控制块先构造完成再接管对象指针,避免中途抛异常泄漏;析构为 noexcept
  • 前置条件:不得用同一裸指针构造多个 shared_ptr;unique_ptr 移动后源为空

三、配套:unique_ptr(独占所有权) ​

实现范围:精简版,覆盖独占语义(禁拷贝、移动、reset / release / get / operator* / operator-> / operator bool)与删除器模板参数。 未实现 T[] 数组特化、operator[]、比较运算符与带状态删除器的完整转发。

cpp
template <class T, class D = std::default_delete<T>>
class UniquePtr {
 public:
  UniquePtr() noexcept = default;
  explicit UniquePtr(T* p) noexcept : p_(p) {}
  UniquePtr(T* p, D d) noexcept : p_(p), d_(std::move(d)) {}

  UniquePtr(const UniquePtr&) = delete;                 // 独占:禁拷贝
  UniquePtr& operator=(const UniquePtr&) = delete;

  UniquePtr(UniquePtr&& o) noexcept : p_(o.p_), d_(std::move(o.d_)) { o.p_ = nullptr; }
  UniquePtr& operator=(UniquePtr&& o) noexcept {
    if (this != &o) { reset(); p_ = o.p_; d_ = std::move(o.d_); o.p_ = nullptr; }
    return *this;
  }
  ~UniquePtr() { reset(); }

  void reset(T* p = nullptr) noexcept {                   // 先析构旧资源,再接管新资源
    if (p_) d_(p_);
    p_ = p;
  }
  T* release() noexcept { T* p = p_; p_ = nullptr; return p; }   // 放弃所有权
  T& operator*() const noexcept { return *p_; }
  T* operator->() const noexcept { return p_; }
  T* get() const noexcept { return p_; }
  explicit operator bool() const noexcept { return p_ != nullptr; }

 private:
  T* p_ = nullptr;
  [[no_unique_address]] D d_{};                           // 空删除器不占空间
};

技术选型 ​

  • 删除器作为模板参数,即类型的一部分
    • 各 unique_ptr 可携带不同删除器,且空删除器可用 [[no_unique_address]] 做到零占用。
    • 与 shared_ptr 相反:后者的删除器存于控制块,属于类型擦除。
  • 禁拷贝、只允许移动
    • 独占语义的直接表达;移动构造标 noexcept,容器搬移时才敢使用移动。
  • release() 与 reset() 职责分离
    • release() 放弃所有权(返回裸指针、不析构);reset() 先析构旧对象再接管新指针。
  • 接管新指针的顺序为「先析构旧资源」
    • 若先接管再析构,等于把刚拿到的新资源删掉。
  • 数组特化另行处理
    • UniquePtr<T[]> 需使用 delete[],且不提供 operator* / operator->,因为元素访问语义不同。

小问答 ​

  • 这个版本差在哪? 无 T[] 数组特化与 operator[]、无比较运算符,删除器未实现有状态版本的完整空基类优化。
  • 与 std::unique_ptr 的差别? 标准版另有 make_unique、数组特化、pointer typedef(支持 fancy pointer)与有状态删除器的优化存储。
  • 与 shared_ptr 的差别? 无控制块、无原子计数;对象大小就是指针(空删除器不占空间),析构可为 constexpr。
  • 移动后源的状态? p_ == nullptr(本版显式置空;标准库只要求「合法但未指定」)。

四、实现:shared_ptr(两档交付) ​

面试节奏:先交精简版(下面 4.1,结构即库实现的结构,约 60 行),再按追问补到完整版(4.2)。

档位控制块形态缺口适用场景
精简版模板化 ControlBlock<T>(强/弱计数 + T*),与对象分两次分配无 weak_ptr(计数已预留)、无 make_shared、无删除器类型擦除(SharedPtr<Base> 不能接 SharedPtr<Derived>)题目只要求「共享所有权 + 引用计数」;时间紧
完整版非模板 ControlBlock 基类 + 虚 dispose,对象与控制块同块分配——追问涉及 weak_ptr、删除器、派生类转换或分配次数时

4.1 精简版:堆上独立控制块(先交这个) ​

实现范围:共享所有权本体 —— 构造/拷贝/移动/析构、reset(无参与接管两种)、swap、get / operator* / operator-> / operator bool / use_count。 控制块为模板化结构、持有 T*;未实现 weak_ptr、make_shared、删除器类型擦除与派生类转换。

cpp
template <class T>
class SharedPtr {
  struct ControlBlock {                    // 堆上独立分配,与对象分开(库实现即此结构)
    std::atomic<long> strong{1};
    std::atomic<long> weak{1};             // 自持一份,为 weak_ptr 预留
    T* obj;
    explicit ControlBlock(T* p) : obj(p) {}

    void release_strong() noexcept {
      // acq_rel:保证此前的写对销毁路径可见,且其他线程不会观察到半销毁状态
      if (strong.fetch_sub(1, std::memory_order_acq_rel) == 1) { delete obj; obj = nullptr; release_weak(); }
    }
    void release_weak() noexcept {
      if (weak.fetch_sub(1, std::memory_order_acq_rel) == 1) delete this;
    }
  };

 public:
  SharedPtr() noexcept = default;
  explicit SharedPtr(T* p) : cb_(p ? new ControlBlock(p) : nullptr) {}   // 接管一个裸指针

  SharedPtr(const SharedPtr& o) noexcept : cb_(o.cb_) {
    if (cb_) cb_->strong.fetch_add(1, std::memory_order_relaxed);        // 仅计数,relaxed 足够
  }
  SharedPtr(SharedPtr&& o) noexcept : cb_(o.cb_) { o.cb_ = nullptr; }
  SharedPtr& operator=(const SharedPtr& o) noexcept {
    if (this != &o) { SharedPtr tmp(o); swap(tmp); }                     // 先拷贝后释放:自赋值安全
    return *this;
  }
  SharedPtr& operator=(SharedPtr&& o) noexcept {
    if (this != &o) { SharedPtr tmp(std::move(o)); swap(tmp); }
    return *this;
  }
  ~SharedPtr() { if (cb_) cb_->release_strong(); }

  void swap(SharedPtr& o) noexcept { std::swap(cb_, o.cb_); }
  void reset() noexcept { SharedPtr tmp; swap(tmp); }
  void reset(T* p) { SharedPtr tmp(p); swap(tmp); }                      // 接管 p 的所有权

  T* get() const noexcept { return cb_ ? cb_->obj : nullptr; }
  T& operator*() const noexcept { return *get(); }
  T* operator->() const noexcept { return get(); }
  explicit operator bool() const noexcept { return cb_ != nullptr; }
  long use_count() const noexcept { return cb_ ? cb_->strong.load(std::memory_order_relaxed) : 0; }

 private:
  ControlBlock* cb_ = nullptr;
};

小问答 ​

  • 这个版本差在哪? 无 weak_ptr、无 make_shared、无删除器类型擦除(控制块模板化,故 SharedPtr<Base> 接不了 SharedPtr<Derived>)、无 enable_shared_from_this。
  • 与 STL 的差别? 库实现的控制块非模板(_Sp_counted_base + 虚 dispose),因此支持别名构造、删除器类型擦除与派生类转换;本版与对象分两次分配,库版可用 make_shared 合并。
  • 为什么这样取舍? 控制块独立 = 与库结构同构、后续扩展有落点;弱计数已预留,加 weak_ptr 不必改结构;总量约 60 行且不含类型擦除的复杂度。
  • 取舍要点? 控制块独立分配而非把计数塞进对象或指针;强计数用原子(递增 relaxed、递减到零 acq_rel);拷贝赋值走「临时对象 + swap」;reset() 与 reset(T*) 分离。
  • 不建议再写更简的版本:单指针计数既不支持 weak_ptr、计数也常写成非原子,而且并不比本版短,省下的行数换来的是无法回答追问。

4.2 完整版:控制块 + SharedPtr + WeakPtr(一个编译单元) ​

实现范围:在 4.1 基础上补入四件事 —— ① 非模板控制块基类 + 虚 dispose(删除器类型擦除, 也是派生类转换的前提);② 对象与控制块同块分配(make_shared);③ WeakPtr 与 lock;④ SharedPtr 只保存控制块指针。构件按依赖顺序排列,可整段复制编译。 未实现别名构造、static_pointer_cast 系列、get_deleter 与 enable_shared_from_this。

cpp
#include <atomic>
#include <cstddef>
#include <new>
#include <utility>

// 控制块:强/弱计数 + 类型擦除的销毁动作
class ControlBlock {
 public:
  std::atomic<long> strong{1};
  std::atomic<long> weak{1};
  virtual void dispose() noexcept = 0;   // 类型擦除:调用正确的 T 析构
  virtual ~ControlBlock() = default;

  void release_strong() noexcept {
    // acq_rel:既保证看到对象内容的写入,也避免其他线程观察到半销毁状态
    if (strong.fetch_sub(1, std::memory_order_acq_rel) == 1) { dispose(); release_weak(); }
  }
  void release_weak() noexcept {
    if (weak.fetch_sub(1, std::memory_order_acq_rel) == 1) delete this;
  }
};

// 单块分配:控制块与 T 的存储位于同一块内存(即 make_shared 省去的那次分配)
template <class T>
struct Cb final : ControlBlock {
  alignas(T) unsigned char buf[sizeof(T)];
  T* ptr;
  template <class... A>
  explicit Cb(A&&... a) : ptr(reinterpret_cast<T*>(buf)) {
    ::new (static_cast<void*>(buf)) T(std::forward<A>(a)...);
  }
  void dispose() noexcept override { ptr->~T(); }
};
template <class T>
class SharedPtr {
  template <class U> friend class WeakPtr;

 public:
  SharedPtr() noexcept = default;
  explicit SharedPtr(ControlBlock* cb) noexcept : cb_(cb) {}   // 接管一个已持有的引用
  SharedPtr(const SharedPtr& o) noexcept : cb_(o.cb_) {
    if (cb_) cb_->strong.fetch_add(1, std::memory_order_relaxed);   // 仅计数,relaxed 足够
  }
  SharedPtr(SharedPtr&& o) noexcept : cb_(o.cb_) { o.cb_ = nullptr; }
  SharedPtr& operator=(const SharedPtr& o) noexcept {
    if (this != &o) { SharedPtr tmp(o); swap(tmp); }             // 先拷贝后释放:自赋值与别名均安全
    return *this;
  }
  SharedPtr& operator=(SharedPtr&& o) noexcept {
    if (this != &o) { SharedPtr tmp(std::move(o)); swap(tmp); }
    return *this;
  }
  ~SharedPtr() { if (cb_) cb_->release_strong(); }

  void swap(SharedPtr& o) noexcept { std::swap(cb_, o.cb_); }
  void reset() noexcept { SharedPtr tmp; swap(tmp); }

  T* get() const noexcept { return cb_ ? static_cast<Cb<T>*>(cb_)->ptr : nullptr; }
  T& operator*() const noexcept { return *get(); }
  T* operator->() const noexcept { return get(); }
  explicit operator bool() const noexcept { return cb_ != nullptr; }
  long use_count() const noexcept { return cb_ ? cb_->strong.load(std::memory_order_relaxed) : 0; }

 private:
  ControlBlock* cb_ = nullptr;
};

template <class T, class... A>
SharedPtr<T> make_shared(A&&... a) {                         // 一次分配
  return SharedPtr<T>(new Cb<T>(std::forward<A>(a)...));
}
template <class T>
class WeakPtr {
 public:
  WeakPtr() noexcept = default;
  WeakPtr(const SharedPtr<T>& sp) noexcept : cb_(sp.cb_) {
    if (cb_) cb_->weak.fetch_add(1, std::memory_order_relaxed);
  }
  WeakPtr(const WeakPtr& o) noexcept : cb_(o.cb_) {
    if (cb_) cb_->weak.fetch_add(1, std::memory_order_relaxed);
  }
  WeakPtr& operator=(const WeakPtr& o) noexcept {
    if (this != &o) { WeakPtr tmp(o); std::swap(cb_, tmp.cb_); }
    return *this;
  }
  ~WeakPtr() { if (cb_) cb_->release_weak(); }

  // 必须先判断 strong != 0 再增 1:分两步(先读后加)时,中间可能被其他线程减到 0 并销毁对象
  SharedPtr<T> lock() const noexcept {
    if (!cb_) return {};
    long c = cb_->strong.load(std::memory_order_relaxed);
    while (c != 0 && !cb_->strong.compare_exchange_weak(c, c + 1, std::memory_order_acq_rel,
                                                        std::memory_order_relaxed)) {
      // CAS 失败时 c 被更新为当前值,继续循环
    }
    return c == 0 ? SharedPtr<T>{} : SharedPtr<T>(cb_);   // 接管该次 CAS 取得的 +1
  }
  bool expired() const noexcept { return !cb_ || cb_->strong.load(std::memory_order_relaxed) == 0; }

 private:
  ControlBlock* cb_ = nullptr;
};

小问答 ​

  • 比精简版多了什么? 虚 dispose(删除器类型擦除)、对象与控制块同块分配、WeakPtr::lock,以及 SharedPtr 只保存控制块指针。
  • 与 STL 还差什么? 别名构造、static_pointer_cast 系列、get_deleter、enable_shared_from_this、分配器支持,以及 std::atomic<std::shared_ptr> 的全部接口。
  • make_shared 的代价? 存在 weak_ptr 时整块内存不能提前释放(对象已析构,内存仍被控制块占住)。
  • 为什么 lock() 必须用 CAS 循环? 提升操作要与「另一线程把强计数减到 0」互斥;先读后加存在中间窗口。

4.3 技术选型(shared_ptr 家族) ​

  • 控制块用虚 dispose 做类型擦除
    • 控制块不能是模板,否则每个 T 都会暴露给所有持有者;虚函数让 shared_ptr<T> 只依赖一个非模板基类。
  • 对象与控制块同块分配(Cb<T> 内嵌 alignas(T) 缓冲)
    • 省一次分配并改善局部性;代价是存在 weak_ptr 时整块内存不能提前释放。
  • 计数用 std::atomic<long>:递增用 relaxed,递减到零用 acq_rel
    • 计数递增不发布任何数据,relaxed 足够;
    • 递减到 0 时需要 acq_rel,以保证此前的写对销毁路径可见,且其他线程不会观察到半销毁状态。
  • 拷贝赋值采用「先拷贝构造临时对象再 swap」
    • 对自赋值与别名均安全,无需额外的判等前置检查。
  • weak_ptr::lock 用 CAS 循环而非「读后加」
    • 提升操作必须与「另一线程把强计数减到 0」互斥;先读后加存在中间窗口。
  • 控制块的释放条件为强、弱计数同时归零
    • 若只判强计数,仍持有 weak_ptr 的线程会在访问控制块时悬垂。
  • SharedPtr 只保存控制块指针,get() 从控制块取对象地址
    • 两个指针可退化为一个,对象地址由控制块的派生类给出。

五、常见错误实现 ​

cpp
// 同一裸指针构造两个 shared_ptr -> 两套控制块、各计数 1 -> double free
T* raw = new T;
SharedPtr<T> a(raw);
SharedPtr<T> b(raw);          // 错误;应从 a 拷贝
cpp
// lock() 写成先读后加:两步之间对象可能已被销毁
if (cb_->strong.load() > 0) { cb_->strong.fetch_add(1); return SharedPtr<T>(cb_); }
cpp
// 拷贝赋值先释放再拷贝:自赋值或别名时会把对象删除
release(); ptr_ = o.ptr_; count_ = o.count_; ++*count_;   // 错误;应先拷贝构造临时再 swap

六、高频问答 ​

Q1. 控制块为什么要虚 dispose(或函数指针)? ​

weak_ptr 与控制块需要类型擦除:控制块不能是模板,否则每个 T 都会暴露给所有持有者。 libstdc++ 用 _Sp_counted_base + virtual _M_dispose(),删除器存放在控制块中。

Q2. make_shared 省了什么,代价是什么? ​

省一次 malloc(对象与控制块同块,缓存更友好);代价是只要还有 weak_ptr, 整块内存就不能释放(对象已析构但内存占用保留),而 shared_ptr(new T) 可更早归还对象内存。

Q3. shared_ptr 的线程安全边界? ​

操作安全性
多线程对同一控制块计数增减(拷贝/析构不同的 shared_ptr 对象)安全(计数为原子)
多线程读写被指向对象不安全
多线程读写同一个 shared_ptr 对象本身不安全(对象本体非原子)

Q4. enable_shared_from_this 的作用? ​

使对象能从 this 安全取得 shared_ptr(内部持有一个 weak_ptr),避免从裸 this 另建控制块而产生两套计数。

Q5. 两者的删除器差异? ​

unique_ptr 的删除器是类型的一部分(模板参数,可空基类优化,各对象可不同); shared_ptr 的删除器存于控制块中(类型擦除),故 T 相同的 shared_ptr 即使删除器不同也可互相拷贝赋值。

Q6. 为什么 weak_ptr 存在时控制块不能提前释放? ​

控制块释放条件为 strong == 0 && weak == 0。若在 strong == 0 时就 delete 控制块, 仍持有 weak_ptr 的线程通过 lock() / expired() 访问控制块即为悬垂。

七、演进路径(4.1 → 4.2) ​

步骤补入的构件解决的缺口
1非模板控制块基类 + 虚 dispose删除器类型擦除;SharedPtr<Base> 能接 SharedPtr<Derived>(4.1 的模板化控制块做不到)
2对象与控制块同块分配(Cb<T> + alignas(T) 缓冲)少一次分配、改善局部性(make_shared 的形态,代价见 Q2)
3WeakPtr + lock() 的 CAS 循环观察语义;提升操作必须与「强计数归零」互斥
4SharedPtr 只存控制块指针,get() 由控制块给出两个指针可退化为一个,对象地址由派生控制块提供

八、常见错误 ​

错误后果
移动构造/赋值后未将源置空double free
拷贝赋值先释放再拷贝自赋值时删除自身资源
同一裸指针构造两个 shared_ptrdouble free
先接管指针再 new 控制块中途抛异常泄漏对象(应先构造控制块再接管)
控制块释放条件写成 strong == 0 即 deleteweak_ptr::lock 访问已释放控制块
lock() 采用「先读后加」与最后一个强引用析构竞态
将 shared_ptr 的删除器写成模板参数语义退化为 unique_ptr,类型不可互换

关联:../smart-pointers/unique_ptr.md、 ../smart-pointers/shared_ptr.md、 ../smart-pointers/weak_ptr.md。

糌粑的面经笔记 · VitePress 版