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