shared_ptr 和unique_ptr实现

shared_ptr 和unique_ptr实现

面试里问“shared_ptr / unique_ptr 怎么实现、怎么写代码”,一般不是要你写出 STL 完整版本,而是考察你是否抓住 所有权语义 + RAII + 拷贝/移动规则 + 引用计数控制块 + 线程安全点。你可以按下面这个“答题套路”讲,再写一个能跑的简化实现(10~20 分钟能写完)。


1)unique_ptr 怎么实现(要点)

  • unique_ptr<T> 只有一个成员:T* ptr;(再加 deleter)
  • 禁止拷贝:拷贝构造/拷贝赋值 = delete
  • 允许移动:移动构造/移动赋值,把指针“转移”,源置空
  • 析构:如果 ptr != nullptrdelete 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:
    • delete ptr
    • delete cb
  • 支持 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;
// ref_cnt--,如果变成 0,释放资源和控制块
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 控制块
    就很加分。

  1. 把上面两个简化实现手写 3 遍(不看稿子写出来)
  2. 每次写完自己回答这几个问题:
    • unique_ptr 为什么 delete copy?
    • shared_ptr 拷贝/析构分别做了什么?
    • 控制块里为什么要 atomic?
    • 循环引用怎么产生,weak_ptr 怎么解?
  3. 再准备 2 个小 demo 现场跑思路:
    • unique_ptr move 后原指针为 nullptr
    • shared_ptr 拷贝 use_count 变化、最后一个析构才 delete