shared_ptr 和unique_ptr实现
面试里问“shared_ptr / unique_ptr 怎么实现、怎么写代码”,一般不是要你写出 STL 完整版本,而是考察你是否抓住 所有权语义 + RAII + 拷贝/移动规则 + 引用计数控制块 + 线程安全点。你可以按下面这个“答题套路”讲,再写一个能跑的简化实现(10~20 分钟能写完)。
1)unique_ptr 怎么实现(要点)
unique_ptr<T> 只有一个成员:T* ptr;(再加 deleter)
- 禁止拷贝:拷贝构造/拷贝赋值
= delete
- 允许移动:移动构造/移动赋值,把指针“转移”,源置空
- 析构:如果
ptr != nullptr 就 delete ptr(或调用 deleter)
- 提供
get() / release() / reset() / operator* / operator->
核心:独占所有权 + 移动语义 + RAII
2)shared_ptr 怎么实现(要点)
shared_ptr<T> 内部有两个指针:
T* ptr 指向对象
ControlBlock* cb 指向控制块(引用计数、deleter)
- 拷贝构造:
++cb->ref_cnt
- 析构:
--cb->ref_cnt,如果减到 0:
- 支持
weak_ptr:控制块里还有 weak_cnt(面试经常加分)
- 线程安全:引用计数通常用
std::atomic<long>(但对象本身访问不自动线程安全)
核心:共享所有权 + 控制块 + 引用计数 + 最后一个释放才 delete
现场写的“简化版”代码(面试够用)
下面是 最常被认可 的写法:能表达语义、能编译、逻辑正确。
(不包含数组版本、不包含自定义 deleter/分配器的全套复杂细节,但足够应对面试)
A) 简化版 unique_ptr(可手写)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| template<class T> class UniquePtr { public: UniquePtr() noexcept : ptr_(nullptr) {} explicit UniquePtr(T* p) noexcept : ptr_(p) {}
UniquePtr(const UniquePtr&) = delete; UniquePtr& operator=(const UniquePtr&) = delete;
UniquePtr(UniquePtr&& other) noexcept : ptr_(other.ptr_) { other.ptr_ = nullptr; } UniquePtr& operator=(UniquePtr&& other) noexcept { if (this != &other) { reset(); ptr_ = other.ptr_; other.ptr_ = nullptr; } return *this; }
~UniquePtr() { reset(); }
T* get() const noexcept { return ptr_; } T& operator*() const { return *ptr_; } T* operator->() const noexcept { return ptr_; }
T* release() noexcept { T* raw = ptr_; ptr_ = nullptr; return raw; }
void reset(T* p = nullptr) noexcept { if (ptr_) delete ptr_; ptr_ = p; }
explicit operator bool() const noexcept { return ptr_ != nullptr; }
private: T* ptr_; };
|
你写完这段,面试官一般会追问:
- “为什么 reset 里要先 delete 再赋值?”
- “移动赋值为什么要 self-check?”
- “release 用来干嘛?”
你都能顺着解释就稳了。
B) 简化版 shared_ptr(核心:控制块 + 引用计数)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
| #include <atomic>
template<class T> class SharedPtr { struct ControlBlock { std::atomic<long> ref_cnt{1}; T* ptr; explicit ControlBlock(T* p) : ptr(p) {} };
public: SharedPtr() noexcept : cb_(nullptr) {} explicit SharedPtr(T* p) : cb_(p ? new ControlBlock(p) : nullptr) {}
SharedPtr(const SharedPtr& other) noexcept : cb_(other.cb_) { if (cb_) cb_->ref_cnt.fetch_add(1, std::memory_order_relaxed); } SharedPtr& operator=(const SharedPtr& other) noexcept { if (this != &other) { release_cb(); cb_ = other.cb_; if (cb_) cb_->ref_cnt.fetch_add(1, std::memory_order_relaxed); } return *this; }
SharedPtr(SharedPtr&& other) noexcept : cb_(other.cb_) { other.cb_ = nullptr; } SharedPtr& operator=(SharedPtr&& other) noexcept { if (this != &other) { release_cb(); cb_ = other.cb_; other.cb_ = nullptr; } return *this; }
~SharedPtr() { release_cb(); }
T* get() const noexcept { return cb_ ? cb_->ptr : nullptr; } T& operator*() const { return *cb_->ptr; } T* operator->() const noexcept { return cb_->ptr; } long use_count() const noexcept { return cb_ ? cb_->ref_cnt.load() : 0; } explicit operator bool() const noexcept { return get() != nullptr; }
private: void release_cb() noexcept { if (!cb_) return; if (cb_->ref_cnt.fetch_sub(1, std::memory_order_acq_rel) == 1) { delete cb_->ptr; delete cb_; } cb_ = nullptr; }
ControlBlock* cb_; };
|
面试官常追问的点(你背这个就能答):
- “为什么 shared_ptr 拷贝不 delete?”
因为只有 ref_cnt 到 0 才 delete。
- “为什么要控制块?”
因为对象地址和计数要共享、且计数要独立生命周期管理。
- “线程安全吗?”
计数加减用 atomic 可以做到基本线程安全;但 对象本身 并不自动线程安全。
面试加分项:你主动补充 weak_ptr 和循环引用
面试官常问:“shared_ptr 的坑是什么?”
你说:
- 循环引用会泄漏:A 持有 B 的 shared_ptr,B 持有 A 的 shared_ptr,计数永远不为 0
- 解决:引入
weak_ptr,弱引用不增加 strong 引用计数
你不需要完整写出 weak_ptr,但能讲控制块里同时有:
strong_count
weak_count
- strong 到 0 delete 对象;weak 到 0 delete 控制块
就很加分。
- 把上面两个简化实现手写 3 遍(不看稿子写出来)
- 每次写完自己回答这几个问题:
- unique_ptr 为什么 delete copy?
- shared_ptr 拷贝/析构分别做了什么?
- 控制块里为什么要 atomic?
- 循环引用怎么产生,weak_ptr 怎么解?
- 再准备 2 个小 demo 现场跑思路:
- unique_ptr move 后原指针为 nullptr
- shared_ptr 拷贝 use_count 变化、最后一个析构才 delete