☰
【C++面试】手写智能指针(二):从shared_ptr理解引用计数与共享所有权
2026/10/4 5:19:57 网站建设 项目流程

一、为什么有了unique_ptr还需要shared_ptr

上一篇手写的unique_ptr强调:

一个资源 ↓ 只能有一个所有者

例如:

MyUniquePtr<int> p1(new int(10));

此时:

p1 ↓ 10

不能再直接:

MyUniquePtr<int> p2 = p1;

因为unique_ptr禁止拷贝。

如果真的要把资源交给p2:

MyUniquePtr<int> p2(std::move(p1));

执行的是:

所有权转移

最终:

p1 → nullptr p2 → 10

但是实际开发中,有时候一块资源确实需要被多个对象共同使用。

例如:

对象A ↓ Resource ↑ 对象B

对象 A 和对象 B 都需要访问同一个资源。

如果仍然使用独占所有权,就会比较麻烦。

于是就有了:

shared_ptr

它允许:

多个智能指针 ↓ 共同拥有同一块资源

例如:

std::shared_ptr<int> p1(new int(10)); std::shared_ptr<int> p2 = p1;

此时:

p1 ──┐ ├──→ 10 p2 ──┘

但这又带来了一个新的问题:

到底应该由谁delete?

如果:

p1

析构的时候直接:

delete ptr_;

那么:

p2

就会变成悬空指针。

所以shared_ptr必须知道:

当前还有多少个shared_ptr 正在使用这块资源

这就是:

引用计数 Reference Count

二、引用计数到底是怎么工作的

假设刚创建:

MySharedPtr<int> p1(new int(10));

此时只有:

p1

管理资源。

所以:

引用计数 = 1

结构可以理解成:

p1 ↓ ptr_ ─────────→ 10 count_ ───────→ 1

现在执行:

MySharedPtr<int> p2 = p1;

因为多了一个共享所有者:

引用计数 1 → 2

此时:

p1 ──┐ ├──→ 10 p2 ──┘ 引用计数 = 2

如果再执行:

MySharedPtr<int> p3 = p2;

那么:

引用计数 2 → 3

现在:

p1 ──┐ p2 ──┼──→ 10 p3 ──┘ 引用计数 = 3

接下来假设:

p3离开作用域

那么:

引用计数 3 → 2

但是:

不能释放资源

因为还有:

p1 p2

正在使用。

接着:

p2离开作用域

引用计数:

2 → 1

还是不能释放。

最后:

p1离开作用域

引用计数:

1 → 0

此时已经没有任何所有者,所以:

delete ptr_;

真正释放资源。

因此shared_ptr的核心逻辑可以总结成:

创建shared_ptr ↓ count = 1 ↓ 发生拷贝 ↓ count++ ↓ 对象析构 ↓ count-- ↓ count == 0 ? ↙ ↘ 是 否 ↓ ↓ 释放资源 不释放

所以手写shared_ptr最关键的就是:

所有共享对象 必须共同看到同一个引用计数

不能每个shared_ptr自己保存:

size_t count_;

因为这样:

p1.count_ = 1 p2.count_ = 1

二者互相不知道对方的存在。

因此简化实现中可以保存:

size_t *count_;

让多个MySharedPtr:

共同指向同一个引用计数

结构变成:

p1.ptr_ ────────┐ ├──→ 10 p2.ptr_ ────────┘ p1.count_ ──────┐ ├──→ 2 p2.count_ ──────┘

这就是引用计数能够共享的原因。

三、拷贝构造、赋值和析构怎么写

先定义一个基本结构:

template <typename T> class MySharedPtr { private: T *ptr_; // 真正管理的对象 size_t *count_; // 共享引用计数 public: explicit MySharedPtr(T *ptr = nullptr) : ptr_(ptr), count_(ptr ? new size_t(1) : nullptr) { } };

例如:

MySharedPtr<int> p1(new int(10));

此时:

ptr_ ↓ 10 count_ ↓ 1

1. 拷贝构造

shared_ptr和unique_ptr最大的区别之一就是:

shared_ptr允许拷贝

所以需要实现:

MySharedPtr(const MySharedPtr &other) : ptr_(other.ptr_), count_(other.count_) { if (count_) ++(*count_); }

例如:

MySharedPtr<int> p1(new int(10)); MySharedPtr<int> p2(p1);

首先:

ptr_ = other.ptr_;

让:

p1 p2

指向同一个对象。

接着:

count_ = other.count_;

让二者共享同一个引用计数。

最后:

++(*count_);

引用计数:

1 → 2

最终:

p1 ──┐ ├──→ 10 p2 ──┘ count = 2

2. 析构函数

析构的时候不能直接:

delete ptr_;

而是应该:

先减少引用计数

因此可以把释放逻辑单独封装成:

void release() { if (!count_) return; --(*count_); if (*count_ == 0) { delete ptr_; delete count_; } }

析构函数直接:

~MySharedPtr() { release(); }

假设:

count = 3

一个对象析构:

3 → 2

不会释放资源。

直到:

1 → 0

才执行:

delete ptr_; delete count_;

为什么除了:

delete ptr_;

还需要:

delete count_;

因为我们的引用计数本身也是:

new size_t(1)

动态申请出来的。

所以引用计数没人使用以后也需要释放。


3. 拷贝赋值

现在考虑:

MySharedPtr<int> p1(new int(10)); MySharedPtr<int> p2(new int(20)); p2 = p1;

赋值之前:

p1 → 10 count = 1 p2 → 20 count = 1

执行:

p2 = p1;

首先不能直接:

p2.ptr_ = p1.ptr_;

因为这样:

20

原来那块资源就没人管理了,会造成:

内存泄漏

所以应该先释放p2原来拥有的资源:

release();

然后再接管:

ptr_ = other.ptr_; count_ = other.count_;

最后:

++(*count_);

完整实现:

MySharedPtr &operator=(const MySharedPtr &other) { if (this != &other) { release(); ptr_ = other.ptr_; count_ = other.count_; if (count_) ++(*count_); } return *this; }

最终:

p1 ──┐ ├──→ 10 p2 ──┘ count = 2

而原来:

20

对应的引用计数已经降为:

0

所以被释放。

这里的:

if (this != &other)

是为了避免:

p1 = p1;

这种自赋值情况。

否则可能先把自己的引用计数减掉甚至释放资源,然后又去访问已经失效的数据。

四、移动语义和完整MySharedPtr实现

虽然shared_ptr支持拷贝,但它同样支持:

移动

例如:

MySharedPtr<int> p1(new int(10)); MySharedPtr<int> p2(std::move(p1));

移动和拷贝最大的区别在于:

拷贝 ↓ 增加一个所有者 ↓ 引用计数 +1

而:

移动 ↓ 只是把当前所有权交给另一个对象 ↓ 所有者数量没有增加

所以移动构造:

MySharedPtr(MySharedPtr &&other) noexcept : ptr_(other.ptr_), count_(other.count_) { other.ptr_ = nullptr; other.count_ = nullptr; }

注意这里:

引用计数不用+1

因为:

p1

已经不再是所有者了。

原来:

p1 → 10 count = 1

移动以后:

p1 → nullptr p2 → 10 count = 1

所有者数量仍然:

1

移动赋值同样如此:

MySharedPtr &operator=(MySharedPtr &&other) noexcept { if (this != &other) { release(); ptr_ = other.ptr_; count_ = other.count_; other.ptr_ = nullptr; other.count_ = nullptr; } return *this; }

这里同样先:

release();

处理自己原来管理的资源。

然后接管:

other

的资源。

为了像普通指针一样使用,还可以继续实现:

T &operator*() const { return *ptr_; } T *operator->() const { return ptr_; } T *get() const { return ptr_; } size_t use_count() const { return count_ ? *count_ : 0; } explicit operator bool() const { return ptr_ != nullptr; }

完整代码如下:

#include <cstddef> #include <iostream> #include <utility> template <typename T> class MySharedPtr { private: T *ptr_; size_t *count_; // 当前对象不再持有资源 void release() { if (!count_) return; // 当前所有者减少一个 --(*count_); // 已经没有任何shared_ptr管理资源 if (*count_ == 0) { delete ptr_; delete count_; } ptr_ = nullptr; count_ = nullptr; } public: // 普通构造 explicit MySharedPtr(T *ptr = nullptr) : ptr_(ptr), count_(ptr ? new size_t(1) : nullptr) { } // 析构 ~MySharedPtr() { release(); } // 拷贝构造:增加一个共享所有者 MySharedPtr(const MySharedPtr &other) : ptr_(other.ptr_), count_(other.count_) { if (count_) ++(*count_); } // 拷贝赋值 MySharedPtr &operator=(const MySharedPtr &other) { if (this != &other) { release(); ptr_ = other.ptr_; count_ = other.count_; if (count_) ++(*count_); } return *this; } // 移动构造:转移所有权,不增加引用计数 MySharedPtr(MySharedPtr &&other) noexcept : ptr_(other.ptr_), count_(other.count_) { other.ptr_ = nullptr; other.count_ = nullptr; } // 移动赋值 MySharedPtr &operator=(MySharedPtr &&other) noexcept { if (this != &other) { release(); ptr_ = other.ptr_; count_ = other.count_; other.ptr_ = nullptr; other.count_ = nullptr; } return *this; } // 支持*p T &operator*() const { return *ptr_; } // 支持p->member T *operator->() const { return ptr_; } // 获取原始指针 T *get() const { return ptr_; } // 获取当前引用计数 size_t use_count() const { return count_ ? *count_ : 0; } // 支持if(p) explicit operator bool() const { return ptr_ != nullptr; } }; class Student { public: void show() { std::cout << "Student::show()" << std::endl; } }; int main() { MySharedPtr<Student> p1(new Student); std::cout << "count = " << p1.use_count() << std::endl; { MySharedPtr<Student> p2 = p1; std::cout << "count = " << p1.use_count() << std::endl; p2->show(); } std::cout << "count = " << p1.use_count() << std::endl; return 0; }

运行过程可以理解成:

创建p1 ↓ count = 1 ↓ p2 = p1 ↓ count = 2 ↓ p2离开作用域 ↓ count = 1 ↓ p1离开作用域 ↓ count = 0 ↓ delete资源

因此整个shared_ptr最核心的逻辑其实就是:

ptr_ + 共享引用计数count_

每发生一次拷贝:

count++

每失去一个所有者:

count--

直到:

count == 0

真正释放资源。

五、面试中shared_ptr最容易追问什么

首先很容易被问:

shared_ptr和unique_ptr最大区别是什么?

可以回答:

unique_ptr表示独占所有权,不允许拷贝,只能通过移动语义转移所有权;shared_ptr表示共享所有权,可以有多个智能指针共同管理同一块资源,通过引用计数决定什么时候真正释放资源。

可以简单记成:

unique_ptr 一个资源 ↓ 一个所有者 shared_ptr 一个资源 ↓ 多个所有者 ↓ 引用计数管理生命周期

第二个常见问题是:

为什么引用计数不能直接写成普通成员变量?

例如:

size_t count_;

因为发生拷贝以后:

p1.count_ = 1 p2.count_ = 1

两个对象拥有的是两个独立计数。

而我们真正需要的是:

p1 ──┐ ├──→ 同一个count p2 ──┘

所以简化实现使用:

size_t *count_;

标准库中的shared_ptr则使用更加完整的:

Control Block 控制块

控制块中通常不只有:

shared引用计数

还会管理:

weak引用计数 删除器 分配器

等信息。

所以真正的std::shared_ptr可以大致理解成:

shared_ptr │ ├── 指向实际对象 │ └── 指向Control Block │ ├── strong count ├── weak count └── deleter等信息

本文为了突出面试中的引用计数原理,只使用:

size_t *count_;

进行了简化。

第三个非常常见的问题是:

shared_ptr的引用计数线程安全吗?

这里要分开说。

标准库的不同shared_ptr对象如果共享同一个控制块:

p1 ─┐ ├→ Control Block p2 ─┘

对控制块中引用计数的增减具有线程安全保证。

但是:

shared_ptr引用计数线程安全

不等于:

它指向的对象线程安全

例如:

std::shared_ptr<int> p = std::make_shared<int>(0);

多个线程同时:

++(*p);

仍然可能产生数据竞争。

所以应该区分:

控制块引用计数 ↓ 有相应线程安全保证 被管理的T对象 ↓ 是否线程安全取决于T本身和访问方式

而我们本文手写的:

size_t *count_;

只是普通整数,并没有做线程同步,因此:

这个MySharedPtr本身不是线程安全版本

如果想进一步处理,可以考虑:

std::atomic<size_t>

维护引用计数。

第四个重点问题就是:

shared_ptr有什么经典问题?

答案就是:

循环引用

例如两个对象:

A ↓ shared_ptr<B> B ↓ shared_ptr<A>

形成:

A ─────────→ B ↑ │ └────────────┘

假设:

A的引用计数 = 1 B的引用计数 = 1

即使外部的shared_ptr已经全部销毁:

A还被B引用 B还被A引用

因此:

A count != 0 B count != 0

最终两个对象都无法释放。

也就是:

循环引用 ↓ 引用计数无法归零 ↓ 内存泄漏

而解决这个问题的关键,就是第三种智能指针:

weak_ptr

weak_ptr可以观察:

shared_ptr管理的资源

但是它:

不会增加强引用计数

这样就可以打破:

shared_ptr之间的循环引用

如果从面试角度总结这一篇,可以直接这样回答:

shared_ptr用于实现共享所有权,多个shared_ptr可以共同管理同一块资源。它内部需要共享一个引用计数,拷贝构造和拷贝赋值时引用计数加一,对象析构时引用计数减一,当强引用计数变为 0 时才真正释放资源。移动操作只是转移已有所有权,因此不会增加引用计数。标准库实现中引用计数通常位于独立的控制块中,而shared_ptr最大的经典问题是循环引用,需要使用weak_ptr解决。

所以:

unique_ptr ↓ 独占所有权 shared_ptr ↓ 共享所有权 ↓ 引用计数 weak_ptr ↓ 弱引用 ↓ 解决循环引用

这三种智能指针的关系也就逐渐串起来了。

0voice · GitHub

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询