Skip to content

手写智能指针(含 shared_ptr 完整版)

这是 C++ 面试 压轴手写题:手写 shared_ptr + 引用计数控制块。考察 RAII、拷贝/移动、原子计数、访问语义。

一、手写 unique_ptr

已在 unique_ptr.md 给出简化版。要点回顾:

  • 禁拷贝、支持 move;
  • 析构 delete;
  • reset(先删旧再接管)、release(放弃所有权并返回);
  • 扩展:自定义删除器 + 数组特化。

二、手写 shared_ptr(重点)

方案 A:引用计数直接放堆上的共享控制块(贴近库实现)

cpp
#include <atomic>
#include <utility>

// 控制块:持有强/弱计数 + 对象指针
struct ControlBlock {
    std::atomic<long> strong{0};
    std::atomic<long> weak{0};
    void*  obj_raw;         // 指针语义:实际存 T*,这里用 T* 泛化更合适,见下
};

template <typename T>
class MySharedPtr {
public:
    // 从一个裸指针接管(创建第一个控制块)
    explicit MySharedPtr(T* p = nullptr) : ptr_(p), cb_(nullptr) {
        if (p) {
            cb_ = new ControlBlock;
            cb_->obj_raw = p;
            cb_->strong.store(1);
            cb_->weak.store(0);
        }
    }

    ~MySharedPtr() { release(); }

    // 拷贝:新加一强引用
    MySharedPtr(const MySharedPtr& o) noexcept : ptr_(o.ptr_), cb_(o.cb_) {
        if (cb_) cb_->strong.fetch_add(1);
    }
    MySharedPtr& operator=(const MySharedPtr& o) noexcept {
        if (this != &o) {
            release();               // 放弃旧所有权
            ptr_ = o.ptr_;
            cb_  = o.cb_;
            if (cb_) cb_->strong.fetch_add(1);
        }
        return *this;
    }

    // 移动:直接把所有权搬过来,更高效
    MySharedPtr(MySharedPtr&& o) noexcept : ptr_(o.ptr_), cb_(o.cb_) {
        o.ptr_ = nullptr;
        o.cb_  = nullptr;
    }
    MySharedPtr& operator=(MySharedPtr&& o) noexcept {
        if (this != &o) {
            release();
            ptr_ = o.ptr_;
            cb_  = o.cb_;
            o.ptr_ = nullptr;
            o.cb_  = nullptr;
        }
        return *this;
    }

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

    void reset() { release(); }
    void reset(T* p) { release(); ptr_ = p; if(p){cb_=new ControlBlock;cb_->obj_raw=p;cb_->strong.store(1);cb_->weak.store(0);} }

private:
    // 释放一个强引用:计数-1;若强=0 则删除对象;若同时弱=0 则删除控制块
    void release() {
        if (!cb_) return;
        if (cb_->strong.fetch_sub(1) == 1) {     // 自己是最后一个强引用
            delete ptr_;                          // 删除对象
            if (cb_->weak.load() == 0) {          // 没有弱引用在等
                delete cb_;
                cb_ = nullptr;
            }
        }
        ptr_ = nullptr;
    }

    T* ptr_;
    ControlBlock* cb_;
};

手写版本缺什么(追问点,能答即满分)

  1. 上面 ControlBlock 用了 void* obj_raw,正确做法是让控制块持有 T*(带模板)或把删除逻辑放进控制块——因为对象类型需要调用正确的析构函数。库实现(libstdc++ _Sp_counted_ptr)通过继承虚基类 _Sp_counted_base,用 virtual dispose() 分发析构与删除器。解释「为什么控制块要虚析构/虚 dispose」是加分项。
  2. make_shared 的单块分配:把对象与控制块合并在一次 new 中(定位在一个 buffer),减少一次分配、改善缓存。手写时可提思路:cb_ 末尾放 T,用 placement new。
  3. weak_ptr 支持:需在控制块中实现 lock()(原子判断 strong>0 → +1),Expired 判断,且控制块释放条件为 strong==0 && weak==0。
  4. 线程安全:计数用原子;release/lock 的“检查-递减/递增”顺序要正确,防止竞态(尤其 concurrent shared_ptr 析构仅靠计数器原子即可,但对象删除与弱引用判断需小心)。
  5. 自定义删除器:unique_ptr 删除器是类型的一部分(模板参数);shared_ptr 的删除器存在控制块里(类型擦除),所以两个 shared_ptr 只要类型 T 相同就能互相拷贝赋值,即使删除器不同——这是两者设计差异点。
  6. 构造 shared_ptr 的类需配合 enable_shared_from_this:类内部持 weak 来自初始化。

错误示范(面试要能指出)

cpp
// ❌ 从同一裸指针造出两个独立 shared_ptr → double free
T* raw = new T;
std::shared_ptr<T> a(raw);
std::shared_ptr<T> b(raw);   // 两套控制块,各计数 1,析构各 delete 一次

正确是从 a 拷贝得 b

三、手写 weak_ptr(了解其 lock 语义)

cpp
// 关键点只在 lock(): 原子判断 strong>0 → 提升
template <typename T>
class MyWeakPtr {
public:
    MySharedPtr<T> lock() const {
        // 需要能原子地: if strong>0 { strong++ ; return shared{ptr,cb} }
        // 在 cb_ 上做原子 load→fetch_add 保护才能无竞态
        return ...; // 思路演示
    }
    bool expired() const { return !cb_ || cb_->strong.load() == 0; }
};

手写题一般到 shared_ptr 为止;weak_ptr 只需讲清 lock 的原子性设计。

四、手写精简版(最短可行答案,适合时间紧)

如果只让「写个能用的、体现引用计数的 shared_ptr」,可用单指针计数(无分离控制块),性能略差但逻辑看得见:

cpp
template <typename T>
class TinySharedPtr {
public:
    explicit TinySharedPtr(T* p = nullptr) : ptr_(p), count_(p ? new long(1) : nullptr) {}
    ~TinySharedPtr() { dec(); }
    TinySharedPtr(const TinySharedPtr& o) : ptr_(o.ptr_), count_(o.count_) {
        if (count_) ++*count_;
    }
    TinySharedPtr& operator=(const TinySharedPtr& o) {
        if (this != &o) { dec(); ptr_=o.ptr_; count_=o.count_; if(count_) ++*count_; }
        return *this;
    }
private:
    void dec() {
        if (count_ && --*count_ == 0) { delete ptr_; delete count_; }
    }
    T* ptr_;
    long* count_;   // 非原子,简化示例;真实用 atomic
};

注意:这个版本只支持 shared 不支持 weak,且计数非线程安全;标准版用独立控制块 + 原子计数 + weak。面试可从「简单版」讲起再演进到「完整版」,展示对设计的理解层次。

C++ 面试八股 · VitePress 版