C++
手写 std::shared_ptr
从控制块、原子强引用计数和 make_shared 同址分配出发,实现复制共享、别名构造与确定性销毁。
发布于 2026年7月23日
手写 std::shared_ptr
从控制块、原子强引用计数和 make_shared 同址分配出发,实现复制共享、别名构造与确定性销毁。
本系列代码使用 C++20 和
oc::handmade命名空间,目标是解释实现机制、复杂度和工程边界,不是替代标准库。智能指针管理生命周期,但不会自动解决业务对象的线程安全。
一、学习目标
- 把对象指针与共享控制块的生命周期分开
- 实现复制共享、移动、reset、use_count 和别名构造
- 理解 make_shared 的同址分配与原子强计数边界
二、前置条件
完成 unique_ptr 篇,熟悉原子操作、虚函数类型擦除、placement new、对齐和异常安全。
Linux/macOS:
g++ -std=c++20 -Wall -Wextra -Wpedantic -Werror smart_ptrs_test.cpp -o smart_ptrs_test
./smart_ptrs_test
Windows PowerShell:
cl /std:c++20 /W4 /WX /EHsc smart_ptrs_test.cpp
.\smart_ptrs_test.exe
三、问题与设计选择
每组共享所有者指向同一控制块。控制块保存原子强计数、原子弱计数和销毁策略;原始指针构造使用指针控制块,make_shared 使用内嵌对象控制块,让对象与计数只分配一次。shared_ptr 另存一个可别名的观察指针。
教学实现覆盖核心所有权语义、复制或移动、观察接口、异常回滚与主要辅助函数;不复刻标准库的完整转换构造矩阵、allocator 定制、enable_shared_from_this、owner 比较器和 ABI 细节。
四、内存布局与核心不变量
强计数等于仍拥有对象的 shared_ptr 数量;强计数从 1 变为 0 的线程恰好销毁一次对象。控制块还有一个代表“强计数非零”的隐式弱引用,因此对象销毁后控制块仍可供 weak_ptr 查询。
判断实现是否正确时,不只数对象指针,还要分别追踪“资源是否活着”“拥有者数量”和“控制块是否活着”。三者的生命周期边界并不相同。
五、核心实现
void release_strong() noexcept {
if (strong_.fetch_sub(1, std::memory_order_acq_rel) == 1) {
destroy_object();
release_weak(); // 释放强所有者持有的隐式弱引用
}
}
shared_ptr(const shared_ptr& other) noexcept
: pointer_(other.pointer_), control_(other.control_) {
if (control_) control_->add_strong();
}
~shared_ptr() {
if (control_) control_->release_strong();
}
这段代码展示最容易写错的状态转换。完整实现放在下一节,并与另外两种智能指针共用同一份 smart_ptrs.hpp。
六、完整教学实现
namespace oc::handmade {
namespace detail {
struct adopt_strong_t {
explicit adopt_strong_t() = default;
};
inline constexpr adopt_strong_t adopt_strong{};
class control_block {
public:
control_block() = default;
control_block(const control_block&) = delete;
control_block& operator=(const control_block&) = delete;
virtual ~control_block() = default;
void add_strong() noexcept {
strong_.fetch_add(1, std::memory_order_relaxed);
}
[[nodiscard]] bool try_add_strong() noexcept {
std::size_t observed = strong_.load(std::memory_order_acquire);
while (observed != 0) {
if (strong_.compare_exchange_weak(
observed,
observed + 1,
std::memory_order_acquire,
std::memory_order_relaxed)) {
return true;
}
}
return false;
}
void release_strong() noexcept {
if (strong_.fetch_sub(1, std::memory_order_acq_rel) == 1) {
destroy_object();
release_weak(); // release the implicit weak reference
}
}
void add_weak() noexcept {
weak_.fetch_add(1, std::memory_order_relaxed);
}
void release_weak() noexcept {
if (weak_.fetch_sub(1, std::memory_order_acq_rel) == 1) {
delete_self();
}
}
[[nodiscard]] std::size_t use_count() const noexcept {
return strong_.load(std::memory_order_acquire);
}
private:
virtual void destroy_object() noexcept = 0;
virtual void delete_self() noexcept = 0;
std::atomic<std::size_t> strong_{1};
// Explicit weak owners plus one implicit weak reference while strong > 0.
std::atomic<std::size_t> weak_{1};
};
template<class T, class Deleter>
class pointer_control_block final : public control_block {
public:
pointer_control_block(T* pointer, Deleter deleter)
: pointer_(pointer), deleter_(std::move(deleter)) {}
private:
void destroy_object() noexcept override {
if (pointer_) {
deleter_(std::exchange(pointer_, nullptr));
}
}
void delete_self() noexcept override { delete this; }
T* pointer_;
[[no_unique_address]] Deleter deleter_;
};
template<class T>
class inplace_control_block final : public control_block {
public:
template<class... Args>
explicit inplace_control_block(Args&&... args) {
std::construct_at(get(), std::forward<Args>(args)...);
alive_ = true;
}
[[nodiscard]] T* get() noexcept {
return std::launder(reinterpret_cast<T*>(storage_));
}
private:
void destroy_object() noexcept override {
if (alive_) {
std::destroy_at(get());
alive_ = false;
}
}
void delete_self() noexcept override { delete this; }
alignas(T) std::byte storage_[sizeof(T)];
bool alive_{};
};
} // namespace detail
template<class T>
class shared_ptr;
template<class T>
class weak_ptr;
template<class T, class... Args>
[[nodiscard]] shared_ptr<T> make_shared(Args&&... args);
template<class T>
class shared_ptr {
public:
using element_type = T;
constexpr shared_ptr() noexcept = default;
constexpr shared_ptr(std::nullptr_t) noexcept {}
template<class U>
requires std::convertible_to<U*, T*>
explicit shared_ptr(U* pointer) : pointer_(pointer) {
if (!pointer) return;
try {
control_ = new detail::pointer_control_block<U, default_delete<U>>(
pointer, default_delete<U>{});
} catch (...) {
default_delete<U>{}(pointer);
pointer_ = nullptr;
throw;
}
}
shared_ptr(const shared_ptr& other) noexcept
: pointer_(other.pointer_), control_(other.control_) {
if (control_) control_->add_strong();
}
template<class U>
requires std::convertible_to<U*, T*>
shared_ptr(const shared_ptr<U>& other) noexcept
: pointer_(other.pointer_), control_(other.control_) {
if (control_) control_->add_strong();
}
template<class U>
shared_ptr(const shared_ptr<U>& owner, T* alias) noexcept
: pointer_(alias), control_(owner.control_) {
if (control_) control_->add_strong();
}
shared_ptr(shared_ptr&& other) noexcept
: pointer_(std::exchange(other.pointer_, nullptr)),
control_(std::exchange(other.control_, nullptr)) {}
template<class U>
requires std::convertible_to<U*, T*>
shared_ptr(shared_ptr<U>&& other) noexcept
: pointer_(std::exchange(other.pointer_, nullptr)),
control_(std::exchange(other.control_, nullptr)) {}
explicit shared_ptr(const weak_ptr<T>& weak);
~shared_ptr() {
if (control_) control_->release_strong();
}
shared_ptr& operator=(const shared_ptr& other) noexcept {
shared_ptr(other).swap(*this);
return *this;
}
shared_ptr& operator=(shared_ptr&& other) noexcept {
shared_ptr(std::move(other)).swap(*this);
return *this;
}
void reset() noexcept { shared_ptr().swap(*this); }
template<class U>
requires std::convertible_to<U*, T*>
void reset(U* pointer) {
shared_ptr(pointer).swap(*this);
}
void swap(shared_ptr& other) noexcept {
using std::swap;
swap(pointer_, other.pointer_);
swap(control_, other.control_);
}
[[nodiscard]] T* get() const noexcept { return pointer_; }
[[nodiscard]] T& operator*() const noexcept { return *pointer_; }
[[nodiscard]] T* operator->() const noexcept { return pointer_; }
[[nodiscard]] explicit operator bool() const noexcept {
return pointer_ != nullptr;
}
[[nodiscard]] std::size_t use_count() const noexcept {
return control_ ? control_->use_count() : 0;
}
private:
shared_ptr(detail::control_block* control, T* pointer, detail::adopt_strong_t)
noexcept
: pointer_(pointer), control_(control) {}
template<class U>
friend class shared_ptr;
template<class U>
friend class weak_ptr;
template<class U, class... Args>
friend shared_ptr<U> make_shared(Args&&... args);
T* pointer_{};
detail::control_block* control_{};
};
template<class T, class... Args>
[[nodiscard]] shared_ptr<T> make_shared(Args&&... args) {
auto* control =
new detail::inplace_control_block<T>(std::forward<Args>(args)...);
return shared_ptr<T>(control, control->get(), detail::adopt_strong);
}
} // namespace oc::handmade
统一测试覆盖移动、复制、数组、别名所有权、对象析构、过期观察者和失败提升;实现没有向 std 命名空间注入任何类型。
七、使用示例与输出
预期输出或状态:
复制后 use_count 从 1 变为 2;副本离开作用域后恢复为 1;最后一个强所有者析构时对象存活计数回到 0。
示例使用断言验证状态,因此成功时不打印额外文本。析构计数必须回到 0,才能说明资源和控制块路径已经闭合。
八、复杂度与失效规则
| 操作 | 复杂度 | 说明 |
|---|---|---|
| 默认/移动构造 | O(1) | 不修改或转移计数 |
| 复制构造/复制赋值 | O(1) | 原子增加强计数 |
| reset/析构 | O(1)+析构 | 最后一个所有者销毁对象 |
| use_count | O(1) | 并发下只是瞬时快照 |
| make_shared | 一次分配+构造 | 对象与控制块同址 |
| 别名构造 | O(1) | 共享所有权但 get() 可不同 |
智能指针本身没有容器迭代器;这里的“失效”指资源销毁后,裸指针、引用或观察者能否继续安全使用。任何从 get()、operator* 或 operator-> 取得的非拥有访问,都不能延长对象生命周期。
九、异常安全与资源管理
- 从裸指针建立控制块若分配失败,必须立即用匹配 deleter 回收原对象。
- 复制赋值使用临时副本再 swap,先成功增加新计数,随后安全释放旧所有权。
make_shared中对象构造失败时,new-expression 自动释放控制块存储。- 引用计数原子化只保护控制块;多个线程修改同一个
shared_ptr对象或其指向对象仍需外部同步。
析构和引用计数释放路径必须为 noexcept。用户 deleter 若从析构路径抛出,程序无法可靠恢复,因此教学实现要求 deleter 的调用和移动不抛异常。
十、常见错误
1. 同一裸指针创建两个控制块
两个互不知情的强计数都会到零,最终造成双重释放。
2. 把 use_count 当同步条件
读取计数后其他线程可以立即复制或释放,它只能用于观测和调试。
3. 误解别名构造
别名指针可能指向子对象;销毁时释放的是控制块拥有的完整对象,而不是 get()。
十一、面试追问
- 控制块里通常保存哪些信息,为什么不只保存一个计数?
make_shared为什么通常只分配一次,它又有什么内存滞留代价?- 不同
shared_ptr实例并发复制为何安全,同一实例并发赋值为何不安全? - 别名构造的所有权指针与观察指针有什么区别?
回答时先说明所有权,再画出对象与控制块,最后给出原子计数、异常和并发边界。
十二、练习与自测
- 为指针控制块增加自定义 deleter
- 实现受约束的派生类到基类复制转换
- 记录对象销毁与控制块释放的不同时间点
自测标准:能够不看代码画出每次复制、移动、reset、lock 和析构后的强弱计数,并解释最后由谁销毁对象、由谁释放控制块。
十三、官方资料与延伸阅读
上一篇:手写 std::unique_ptr | 下一篇:手写 std::weak_ptr