从零实现C++智能指针:深入理解shared_ptr核心机制与工程实践 1. 项目概述为什么我们要“手撕”一个shared_ptr如果你写过C尤其是写过一些需要手动管理内存的代码那么对“野指针”、“内存泄漏”和“重复释放”这几个词一定不会陌生。它们就像程序里的幽灵平时潜伏着一旦发作轻则程序崩溃数据错乱重则让你在调试器前枯坐数小时怀疑人生。std::shared_ptr作为C11引入的智能指针三巨头之一它的核心价值就是通过“引用计数”的机制自动化地管理对象的生命周期从根本上杜绝这类问题。简单来说它让多个“智能指针”可以安全地共享同一个对象当最后一个指向该对象的shared_ptr被销毁时它才会自动清理内存。那么我们为什么还要自己动手实现一个呢直接用标准库的不香吗香当然香。但“手撕”一个简化版的MySharedPtr其意义远不止于“造轮子”。这更像是一次深入核心的“外科手术”目的是彻底搞懂智能指针的运作机理。你会清晰地看到引用计数如何被创建、增加、减少和归零你会理解拷贝构造函数和赋值运算符重载在这里扮演的关键角色你会直面多线程环境下的数据竞争问题并思考解决方案。这个过程能让你对RAII资源获取即初始化思想、拷贝控制、模板编程有刻骨铭心的理解。当你下次使用std::shared_ptr时你心里是有底的你知道它内部大概在做什么出了问题你也能有更清晰的排查思路。这就是“手撕代码”的价值——不是为了替代而是为了征服。2. 核心设计拆解一个shared_ptr的骨架要自己实现一个MySharedPtr我们首先得把它大卸八块看看标准实现里有哪些核心部件。根据C标准库的典型实现和公开资料比如cppreference一个shared_ptr主要包含两部分数据存储的指针Stored Pointer 这是我们通过get()方法获取到的那个指针也是我们使用-和*运算符操作的对象。它直接指向我们管理的用户数据。控制块指针Control Block Pointer 这是一个指向动态分配的控制块的指针。这个控制块才是智能指针的“大脑”它至少包含引用计数器Use Count 记录当前有多少个shared_ptr正共享着这个对象。当计数器减到0时销毁被管理的对象。弱引用计数器Weak Count 记录有多少个weak_ptr在观察这个对象。这个计数器主要用来决定控制块本身何时能被销毁当引用计数和弱引用计数都归零时。删除器Deleter 一个可调用对象用于销毁被管理的对象。默认是delete操作符但用户可以自定义这对于管理数组delete[]或特殊资源如文件句柄、网络套接字至关重要。分配器Allocator 用于分配控制块和对象内存的分配器通常可以忽略使用默认的new/delete。注意 在std::make_shared创建时为了优化控制块和被管理对象的内存通常是一次性分配的对象就构造在控制块之后。但我们自己实现时为了简化可以先采用“控制块和对象分开两次分配”的策略这样逻辑更清晰。我们的MySharedPtr将聚焦最核心的功能基于引用计数的共享所有权、基本的RAII、拷贝语义以及自定义删除器支持。我们会先实现一个单线程安全的版本然后再讨论多线程带来的挑战。2.1 定义我们的类模板骨架首先我们搭建起类的框架。它是一个模板类以适配任何类型T。templatetypename T class MySharedPtr { private: // 核心数据成员 T* ptr_; // 存储的指针指向用户数据 ControlBlock* cb_; // 指向控制块的指针 // 辅助函数增加引用计数 void incrementRefCount() { if (cb_) { (cb_-use_count); } } // 辅助函数减少引用计数并在必要时清理资源 void decrementRefCount() { if (cb_) { --(cb_-use_count); if (cb_-use_count 0) { // 销毁管理的对象 if (cb_-deleter) { cb_-deleter(ptr_); } else { delete ptr_; } // 如果弱引用计数也为0则销毁控制块本身 if (cb_-weak_count 0) { delete cb_; } } // 注意ptr_和cb_的置空在reset或析构函数中处理更合适 } } public: // 构造函数们、析构函数、拷贝控制函数... // operator*, operator-, get(), use_count()等接口... };这里出现了一个未定义的ControlBlock。接下来我们就需要设计这个核心结构。2.2 设计控制块Control Block控制块需要以某种形式保存引用计数、弱引用计数和删除器。删除器类型可能五花八门函数指针、函数对象、lambda等我们需要用到类型擦除技术来存储它。一个简单的方法是使用std::function但为了更贴近底层和理解原理我们可以定义一个基类然后通过模板派生类来保存特定类型的删除器。// 控制块基类定义接口 struct ControlBlockBase { size_t use_count; // 共享引用计数 size_t weak_count; // 弱引用计数 virtual ~ControlBlockBase() default; virtual void destroy(void* ptr) 0; // 纯虚函数用于销毁对象 }; // 模板派生类保存特定类型的删除器 templatetypename T, typename Deleter std::default_deleteT struct ControlBlockImpl : public ControlBlockBase { Deleter deleter; // 保存的删除器对象 ControlBlockImpl(Deleter d) : deleter(std::move(d)) { use_count 1; // 创建时共享计数为1 weak_count 0; // 初始弱引用为0 } void destroy(void* ptr) override { // 调用保存的删除器删除正确类型的指针 deleter(static_castT*(ptr)); } };这样在我们的MySharedPtr内部只需要持有一个ControlBlockBase*。当需要销毁对象时通过多态调用destroy方法。这种设计将删除器的具体类型信息隐藏在了控制块实现中实现了类型擦除。3. 关键实现一步步填充血肉有了骨架我们现在开始实现最关键的几个成员函数。这是整个智能指针逻辑的核心。3.1 构造函数与资源初始化一个智能指针首先要解决的就是“从哪里来”的问题。我们需要实现几种常见的构造方式默认构造函数创建一个空的不拥有任何资源的智能指针。原生指针构造函数从一个裸指针接管资源所有权。这是最需要小心的要确保一个资源只被一个控制块管理。拷贝构造函数从另一个MySharedPtr构造共享所有权引用计数加一。移动构造函数接管另一个MySharedPtr的资源原指针置空。templatetypename T class MySharedPtr { // ... 私有成员 ... public: // 1. 默认构造函数 MySharedPtr() noexcept : ptr_(nullptr), cb_(nullptr) {} // 2. 从原生指针构造带默认删除器 explicit MySharedPtr(T* raw_ptr) : ptr_(raw_ptr), cb_(nullptr) { if (raw_ptr) { // 分配控制块使用默认删除器 std::default_deleteT cb_ new ControlBlockImplT(std::default_deleteT{}); } // 如果raw_ptr是nullptrcb_也是nullptr表示空指针 } // 3. 从原生指针和自定义删除器构造 templatetypename Deleter MySharedPtr(T* raw_ptr, Deleter d) : ptr_(raw_ptr), cb_(nullptr) { if (raw_ptr) { // 分配控制块传入自定义删除器 cb_ new ControlBlockImplT, Deleter(std::move(d)); } } // 4. 拷贝构造函数 MySharedPtr(const MySharedPtr other) noexcept : ptr_(other.ptr_), cb_(other.cb_) { incrementRefCount(); // 共享所有权引用计数1 } // 5. 移动构造函数 MySharedPtr(MySharedPtr other) noexcept : ptr_(other.ptr_), cb_(other.cb_) { // 接管资源 other.ptr_ nullptr; other.cb_ nullptr; // 原指针不再拥有任何资源 } // ... 其他成员函数 ... };实操心得在原生指针构造函数中我们使用了explicit关键字。这非常重要它防止了隐式类型转换避免了MySharedPtrint p new int(42);这种容易引发误解的写法强制要求写成MySharedPtrint p(new int(42));提高了代码安全性。3.2 析构函数与资源释放析构函数的职责很明确减少引用计数并检查是否需要释放资源。~MySharedPtr() { decrementRefCount(); // 调用我们之前定义的辅助函数 // 注意decrementRefCount 内部已经处理了对象和控制块的销毁。 // 在当前简单实现中我们不需要再将ptr_和cb_置空因为对象即将销毁。 // 但在更复杂的实现中为了安全有时会在递减计数后立即置空。 ptr_ nullptr; cb_ nullptr; }decrementRefCount函数的逻辑是整个引用计数管理的核心让我们再仔细审视一下void decrementRefCount() { if (cb_) { --(cb_-use_count); if (cb_-use_count 0) { // 这是最后一个shared_ptr销毁管理的对象 cb_-destroy(ptr_); // 通过多态调用正确的删除器 ptr_ nullptr; // 对象已销毁存储指针置空 // 现在检查控制块本身是否需要销毁 // 只有当弱引用计数也为0时才能销毁控制块 if (cb_-weak_count 0) { delete cb_; // 释放控制块内存 } cb_ nullptr; } } }3.3 拷贝赋值与移动赋值运算符赋值操作比构造函数复杂因为它需要先清理this当前持有的资源再接管新的资源。// 拷贝赋值运算符 MySharedPtr operator(const MySharedPtr other) noexcept { // 经典的“拷贝并交换” (copy-and-swap) idiom 可以简化异常安全处理。 // 但为了清晰展示逻辑我们分步实现 if (this ! other) { // 自赋值检查 // 1. 先释放当前持有的资源 decrementRefCount(); // 2. 接管新资源 ptr_ other.ptr_; cb_ other.cb_; // 3. 增加新资源的引用计数 incrementRefCount(); } return *this; } // 移动赋值运算符 MySharedPtr operator(MySharedPtr other) noexcept { if (this ! other) { // 释放当前资源 decrementRefCount(); // 接管移动新资源 ptr_ other.ptr_; cb_ other.cb_; // 将源对象置为空状态 other.ptr_ nullptr; other.cb_ nullptr; } return *this; }注意事项自赋值检查if (this ! other)是必须的。否则在a a这样的操作中我们会先释放a的资源然后再试图去访问已经被释放的other其实就是a自己的资源导致未定义行为。3.4 提供基本接口为了让我们的MySharedPtr好用需要实现一些标准接口。// 解引用操作符 T operator*() const noexcept { // 通常标准库实现不会在operator*里检查空指针行为类似裸指针。 // 但为了安全我们可以添加断言。这里选择类似标准库的行为。 return *ptr_; } // 箭头操作符 T* operator-() const noexcept { return ptr_; } // 获取原始指针 T* get() const noexcept { return ptr_; } // 获取引用计数 size_t use_count() const noexcept { return cb_ ? cb_-use_count : 0; } // 布尔转换用于条件判断 explicit operator bool() const noexcept { return ptr_ ! nullptr; } // 重置智能指针 void reset(T* new_ptr nullptr) { // 释放当前资源 decrementRefCount(); // 接管新资源或置空 ptr_ new_ptr; if (new_ptr) { cb_ new ControlBlockImplT(std::default_deleteT{}); } else { cb_ nullptr; } }4. 深入探讨多线程安全与循环引用我们的基础版本已经可以工作了但在实际生产环境中还有两个至关重要的问题需要面对。4.1 多线程安全挑战标准库的std::shared_ptr保证不同的shared_ptr实例即使它们指向同一个对象的拷贝、赋值、析构等操作是线程安全的。这意味着引用计数的增减必须是原子操作。我们的简单实现使用和--操作这在多线程环境下是不安全的会导致数据竞争和引用计数错误。要让我们的MySharedPtr线程安全必须将控制块中的引用计数改为原子变量。#include atomic struct ControlBlockBase { std::atomicsize_t use_count; // 原子引用计数 std::atomicsize_t weak_count; // 原子弱引用计数 virtual ~ControlBlockBase() default; virtual void destroy(void* ptr) 0; }; // 在 incrementRefCount 和 decrementRefCount 中使用原子操作 void incrementRefCount() { if (cb_) { cb_-use_count.fetch_add(1, std::memory_order_relaxed); } } void decrementRefCount() { if (cb_) { // 注意fetch_sub 返回的是操作之前的值 if (cb_-use_count.fetch_sub(1, std::memory_order_acq_rel) 1) { // 我们是最后一个持有者 cb_-destroy(ptr_); ptr_ nullptr; // 弱引用计数的检查也需要原子操作 if (cb_-weak_count.load(std::memory_order_acquire) 0) { delete cb_; } cb_ nullptr; } } }这里使用了std::memory_order_relaxed和std::memory_order_acq_rel等内存序。简单来说fetch_add用relaxed序因为只是增加计数不依赖其他内存操作。fetch_sub用acq_rel序因为减到0后需要销毁对象这是一个“释放”操作确保对象销毁前的所有写操作对销毁操作可见同时对于后续可能访问该内存的其他线程这是一个“获取”操作。重要提示即使引用计数是原子的通过同一个shared_ptr实例进行非const成员函数调用如reset()也不是线程安全的。标准库也是如此规定的。线程安全仅限于对控制块本身的操作。4.2 循环引用问题与weak_ptr这是shared_ptr的经典陷阱。考虑以下场景struct Node { MySharedPtrNode next; MySharedPtrNode prev; }; auto node1 MySharedPtrNode(new Node); auto node2 MySharedPtrNode(new Node); node1-next node2; // node2 被 node1-next 和 node2 本身引用 node2-prev node1; // node1 被 node2-prev 和 node1 本身引用当node1和node2离开作用域时它们的引用计数都从2减为1因为彼此还互相指着永远不会变为0导致内存泄漏。这就是循环引用。解决方案是引入weak_ptr。weak_ptr是一种“弱引用”它不增加对象的引用计数只“观察”对象。它需要通过lock()方法尝试获取一个临时的shared_ptr来访问对象如果对象还存在的话。在循环引用中将其中一个指针改为weak_ptr即可打破循环。实现一个完整的MyWeakPtr是另一个大话题它需要持有控制块的指针。不增加use_count但会增加weak_count。lock()方法检查use_count是否大于0如果是则创建一个新的MySharedPtr并增加use_count。在我们的MySharedPtr实现中我们已经为weak_count预留了位置并为控制块在weak_count为0时才销毁的逻辑打下了基础这正是一个支持weak_ptr的控制块所必需的。5. 常见问题与调试技巧实录自己实现智能指针的过程中你会遇到各种意想不到的bug。这里记录几个典型问题和排查思路。5.1 引用计数错误多1或少1这是最常见的问题。症状通常是对象该销毁时没销毁泄漏或者不该销毁时被提前销毁悬空指针。排查方法打印日志在每个会改变引用计数的函数构造、拷贝、赋值、析构、reset开始和结束时打印this指针、ptr_值和当前的use_count。这是最直接有效的方法。检查自赋值确保拷贝赋值运算符正确处理了a a的情况。检查移动语义确保移动构造函数和移动赋值运算符正确地将源对象置空避免“双杀”两个指针都认为拥有资源导致重复释放。画图对于复杂的指针关系在纸上画出每个MySharedPtr对象及其指向的控制块手动模拟引用计数的变化。5.2 自定义删除器不工作如果你的对象没有以预期的方式被销毁比如数组该用delete[]却用了delete。排查方法确认你的ControlBlockImpl模板是否正确捕获并存储了删除器的类型。在ControlBlockImpl::destroy方法中使用调试器或打印语句确认调用的确实是传入的删除器。对于数组测试时可以使用一个简单的类在析构函数中打印信息看是否被正确调用。5.3 在多线程测试中发生崩溃即使使用了原子计数程序依然可能崩溃。排查方法确保原子操作的内存序正确。错误的memory_order可能导致某些线程看不到最新的引用计数值。检查是否有线程在通过同一个MySharedPtr实例调用reset()。记住这需要外部同步如加锁。使用线程检查工具如Valgrind的Helgrind、TSanThreadSanitizer来检测数据竞争。在编译时添加-fsanitizethread选项GCC/Clang是极好的实践。5.4 与标准库混用导致的未定义行为绝对不要用你的MySharedPtr和std::shared_ptr混合管理同一个裸指针资源。它们各自有自己的控制块会导致重复释放。这是智能指针使用的铁律一个资源一个控制块。6. 完整代码示例与测试下面是一个整合了上述核心思想的简化版MySharedPtr实现包含自定义删除器但暂未实现多线程原子操作和weak_ptr以保持代码清晰便于理解。#include iostream #include memory // 为了 std::default_delete // 控制块基类 struct ControlBlockBase { size_t use_count; size_t weak_count; // 为weak_ptr预留 virtual ~ControlBlockBase() default; virtual void destroy(void* ptr) 0; }; // 控制块实现类模板保存删除器 templatetypename T, typename Deleter struct ControlBlockImpl : public ControlBlockBase { Deleter deleter; ControlBlockImpl(Deleter d) : deleter(std::move(d)) { use_count 1; weak_count 0; } void destroy(void* ptr) override { deleter(static_castT*(ptr)); } }; // 主模板类 MySharedPtr templatetypename T class MySharedPtr { private: T* ptr_; ControlBlockBase* cb_; void incrementRefCount() { if (cb_) { (cb_-use_count); } } void decrementRefCount() { if (cb_) { --(cb_-use_count); if (cb_-use_count 0) { cb_-destroy(ptr_); ptr_ nullptr; if (cb_-weak_count 0) { delete cb_; } cb_ nullptr; } } } public: // 构造函数 MySharedPtr() noexcept : ptr_(nullptr), cb_(nullptr) {} explicit MySharedPtr(T* raw_ptr) : ptr_(raw_ptr), cb_(nullptr) { if (raw_ptr) { cb_ new ControlBlockImplT, std::default_deleteT(std::default_deleteT{}); } } templatetypename Deleter MySharedPtr(T* raw_ptr, Deleter d) : ptr_(raw_ptr), cb_(nullptr) { if (raw_ptr) { cb_ new ControlBlockImplT, Deleter(std::move(d)); } } // 拷贝构造与赋值 MySharedPtr(const MySharedPtr other) noexcept : ptr_(other.ptr_), cb_(other.cb_) { incrementRefCount(); } MySharedPtr operator(const MySharedPtr other) noexcept { if (this ! other) { decrementRefCount(); ptr_ other.ptr_; cb_ other.cb_; incrementRefCount(); } return *this; } // 移动构造与赋值 MySharedPtr(MySharedPtr other) noexcept : ptr_(other.ptr_), cb_(other.cb_) { other.ptr_ nullptr; other.cb_ nullptr; } MySharedPtr operator(MySharedPtr other) noexcept { if (this ! other) { decrementRefCount(); ptr_ other.ptr_; cb_ other.cb_; other.ptr_ nullptr; other.cb_ nullptr; } return *this; } // 析构函数 ~MySharedPtr() { decrementRefCount(); } // 访问接口 T operator*() const noexcept { return *ptr_; } T* operator-() const noexcept { return ptr_; } T* get() const noexcept { return ptr_; } size_t use_count() const noexcept { return cb_ ? cb_-use_count : 0; } explicit operator bool() const noexcept { return ptr_ ! nullptr; } void reset(T* new_ptr nullptr) { decrementRefCount(); ptr_ new_ptr; if (new_ptr) { cb_ new ControlBlockImplT, std::default_deleteT(std::default_deleteT{}); } else { cb_ nullptr; } } }; // 一个简单的测试类 struct Widget { int id; Widget(int i) : id(i) { std::cout Widget id constructed.\n; } ~Widget() { std::cout Widget id destructed.\n; } void print() const { std::cout Im Widget id std::endl; } }; int main() { std::cout Test 1: Basic RAII \n; { MySharedPtrWidget p1(new Widget(1)); std::cout use_count: p1.use_count() std::endl; // 应为1 { MySharedPtrWidget p2 p1; // 拷贝构造 std::cout use_count after copy: p1.use_count() std::endl; // 应为2 p2-print(); } // p2析构 std::cout use_count after p2 destroyed: p1.use_count() std::endl; // 应为1 } // p1析构Widget 1 应被销毁 std::cout ---\n; std::cout Test 2: Custom Deleter \n; { // 使用一个lambda作为删除器它什么也不做模拟特殊资源清理 auto deleter [](Widget* w) { std::cout Custom deleter called for Widget w-id \n; delete w; // 最终还是需要delete }; MySharedPtrWidget p3(new Widget(2), deleter); } // 应调用自定义删除器 std::cout ---\n; std::cout Test 3: Move Semantics \n; { MySharedPtrWidget p4(new Widget(3)); MySharedPtrWidget p5 std::move(p4); // 移动构造 std::cout p4 after move is null? !p4 std::endl; // 应为 true (1) std::cout p5 use_count: p5.use_count() std::endl; // 应为1 } // Widget 3 应被p5销毁 std::cout ---\n; std::cout Test 4: Reset \n; { MySharedPtrWidget p6(new Widget(4)); p6.reset(new Widget(5)); // Widget 4 应在此处被销毁 std::cout After reset, use_count: p6.use_count() std::endl; } // Widget 5 应在此处被销毁 std::cout All tests finished \n; return 0; }运行这个测试程序你可以清晰地看到对象的构造和析构顺序以及引用计数的变化直观地验证我们MySharedPtr的正确性。自己动手实现一遍你会对std::shared_ptr的敬畏之心油然而生——它要在保证接口简洁、功能强大、性能高效的同时处理好线程安全、异常安全、类型擦除、循环引用等一大堆棘手问题。但这趟“手撕”之旅无疑是值得的它带给你的对C内存管理和对象生命周期的深刻理解是任何书本都难以替代的。下次当你优雅地写下std::shared_ptr时你指尖流淌的不仅是代码还有对整个机制了然于胸的底气。