广告

C++从零实现一个智能指针:手动实现 shared_ptr 的引用计数机制

1. 设计目标与范围

设计目标

在 C++ 中,智能指针的核心能力是通过引用计数实现资源的自动管理。本文从零开始实现一个简化版的 SharedPtr,聚焦于引用计数机制正确的拷贝与释放语义,以及对原始资源的透明访问。通过这套实现,你可以清晰看到资源生命周期是如何被多份指针共同管理的。

该实现的目标是让你理解:控制块如何承载指针和计数、拷贝时如何让计数增加、析构时如何让资源在最后一个引用被释放时才回收,以及如何提供与原生指针等价的接口以减少学习成本。

实现边界与约定

我们仅实现最常见的场景:独享所有权资源的共享,不涉及复杂的 weak_ptr、循环引用检测等高级特性。线程安全也被作为可选扩展点提出,在单线程场景下给出一个简单实现即可。

C++从零实现一个智能指针:手动实现 shared_ptr 的引用计数机制

通过这套边界,我们可以把复杂度聚焦在引用计数更新逻辑资源释放时机、以及对外暴露的访问接口上,从而帮助读者快速理解从零实现一个智能指针的关键步骤。

2. 数据结构设计

核心结构

核心设计思路是把资源指针和引用计数放在一个独立的控制块中,多个 SharedPtr 实例共享同一个控制块,从而实现对资源的引用计数管理。控制块通常包含两个要素:原始指针和一个引用计数值。

将引用计数与资源对象解耦,可以让拷贝、移动和释放操作只触及控制块,从而避免对源对象本身造成污染,也方便未来扩展为弱引用等高级特性。

接口设计

实现的 Public 接口要与标准库的 std::shared_ptr 相近,包括:构造析构拷贝构造拷贝赋值解引用获取原始指针、以及 use_count 等辅助方法。

为了可读性,控制块通常作为 SharedPtr 的私有实现细节存在,外部只通过指针语义与操作符实现与原生指针等价的体验。

3. 代码实现:控制块与引用计数

控制块设计

控制块承担两项职责:维护原始指针维护引用计数。简单版本中,控制块不涉及弱引用,且计数在单线程环境下为普通整型。下面给出一个最小可工作版本的控制块结构。

template<typename T>
struct ControlBlock {T* ptr;size_t ref_count;ControlBlock(T* p): ptr(p), ref_count(1) {}
};

通过该控制块,SharedPtr 的拷贝构造会将引用计数+1,析构时会将计数-1并在计数为 0 时释放资源和控制块本身。这样的分离式设计,是实现可共享引用计数智能指针的核心思路。

4. 代码实现:SharedPtr 类

构造与析构

SharedPtr 应提供默认构造、通过原始指针构造、拷贝构造与析构等基本操作。析构时需要确保对控制块的引用计数进行合理更新,若计数降为 0,则释放资源。

template<typename T>
class SharedPtr {
public:// 默认构造SharedPtr(): ctrl(nullptr) {}// 通过原始指针构造explicit SharedPtr(T* p): ctrl(new ControlBlock<T>(p)) {}// 拷贝构造SharedPtr(const SharedPtr<T>& other) {ctrl = other.ctrl;if (ctrl) ++(ctrl->ref_count);}// 析构~SharedPtr() {release();}private:struct ControlBlockBase {virtual ~ControlBlockBase() = default;virtual void release() = 0;};template<typename U>struct ControlBlock : ControlBlockBase {U* ptr;size_t ref_count;ControlBlock(U* p): ptr(p), ref_count(1) {}void release() override { if (--ref_count == 0) { delete ptr; delete this; } }};ControlBlockBase* ctrl;void release() {if (ctrl) {ctrl->release();ctrl = nullptr;}}
};

以上实现演示了一个较为简化的控制块绑定模式:控制块通常是多态类型,以便将不同模板参数的指针都包裹在同一管理结构中。实际使用中,可以将控制块内的信息设计得更紧凑以减少虚函数开销。

5. 拷贝、移动、重置的语义与实现

拷贝语义

在拷贝构造和拷贝赋值中,需要确保引用计数的正确自增以及避免资源重复释放。若新对象成功构造,则原对象应保持不变,直到最终引用被释放。

// 拷贝构造
SharedPtr(const SharedPtr<T>& other) {ctrl = other.ctrl;if (ctrl) ++(static_castref_count);
}// 拷贝赋值(简化版)
SharedPtr& operator=(const SharedPtr<T>& other) {if (this != &other) {release();ctrl = other.ctrl;if (ctrl) ++(static_castref_count);}return *this;
}

通过把释放逻辑集中在一个私有方法中,可以确保在异常路径下也能正确处理资源回收与计数更新。

6. 访问接口与使用示例

解引用与访问

对外暴露的接口需要实现常见的解引用运算符,以及获取原始指针的能力,以便在语法层面尽量接近原生指针的使用体验。解引用运算符箭头运算符、以及 get()use_count() 等方法是最直观的入口。

template<typename T>
T& operator*() const { return * ctrl->ptr; }
T* operator->() const { return ctrl->ptr; }
T* get() const { return ctrl ? ctrl->ptr : nullptr; }
size_t use_count() const { return ctrl ? ctrl->ref_count : 0; }

使用示例演示了如何在不引入额外所有权语义的前提下,通过简单的操作符实现对目标对象的访问与生命周期同步。

7. 线程安全与扩展

原子性与并发

在单线程场景下,非原子计数的实现已经足够直观且高效。若希望在多线程环境中安全地进行共享,请引入原子性保障。引用计数应变为原子类型,并在拷贝、析构以及重置路径中使用原子操作(如 fetch_add/fetch_sub)来避免竞态条件。

一个可扩展的方向是将控制块分离为独立的模板结构,提供 WeakPtrenable_shared_from_this 等特性,逐步接近标准库实现的能力边界。

#include <atomic>
template<typename T>
struct AtomicControlBlock {T* ptr;std::atomic<size_t> ref_count;AtomicControlBlock(T* p): ptr(p), ref_count(1) {}
};

广告

后端开发标签