在C++中实现自定义智能指针的原理与实践

在现代 C++ 开发中,智能指针是内存管理的重要工具。标准库提供了 std::unique_ptrstd::shared_ptrstd::weak_ptr,但在某些特定场景下,开发者仍可能需要自定义自己的智能指针。本文将从原理出发,讲解如何实现一个简易的 MySharedPtr,并在此基础上演示如何加入线程安全、异常安全以及自定义删除器等功能。


1. 需求与设计目标

功能 说明
共享所有权 std::shared_ptr,允许多份指针引用同一资源。
引用计数 采用计数机制,资源在计数归零时释放。
线程安全 计数操作使用 std::atomic,确保多线程访问不出错。
自定义删除器 允许用户提供自定义删除逻辑(例如 delete[]、文件句柄关闭等)。
异常安全 在构造与销毁过程中保持异常安全,避免内存泄漏。

2. 关键实现细节

2.1 内部控制块(Control Block)

控制块(ControlBlock)保存两件事:

  1. 引用计数(`std::atomic ref_count`)
  2. 删除器std::function<void(T*)> deleter
template<typename T>
struct ControlBlock
{
    std::atomic <size_t> ref_count{1};
    std::function<void(T*)> deleter;

    explicit ControlBlock(std::function<void(T*)> del)
        : deleter(std::move(del)) {}
};

2.2 MySharedPtr

template<typename T>
class MySharedPtr
{
public:
    // 构造
    explicit MySharedPtr(T* ptr = nullptr,
                         std::function<void(T*)> deleter = std::default_delete<T>())
        : ptr_(ptr), ctrl_(nullptr)
    {
        if (ptr_) {
            ctrl_ = new ControlBlock <T>(std::move(deleter));
        }
    }

    // 复制构造
    MySharedPtr(const MySharedPtr& other) noexcept
        : ptr_(other.ptr_), ctrl_(other.ctrl_)
    {
        inc_ref();
    }

    // 移动构造
    MySharedPtr(MySharedPtr&& other) noexcept
        : ptr_(other.ptr_), ctrl_(other.ctrl_)
    {
        other.ptr_ = nullptr;
        other.ctrl_ = nullptr;
    }

    // 赋值
    MySharedPtr& operator=(MySharedPtr other) noexcept
    {
        swap(other);
        return *this;
    }

    // 析构
    ~MySharedPtr()
    {
        dec_ref();
    }

    // 访问
    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 ctrl_ ? ctrl_->ref_count.load() : 0; }

    // 重置
    void reset(T* ptr = nullptr,
               std::function<void(T*)> deleter = std::default_delete<T>())
    {
        MySharedPtr temp(ptr, std::move(deleter));
        swap(temp);
    }

    void swap(MySharedPtr& other) noexcept
    {
        std::swap(ptr_, other.ptr_);
        std::swap(ctrl_, other.ctrl_);
    }

private:
    void inc_ref() noexcept
    {
        if (ctrl_) ctrl_->ref_count.fetch_add(1, std::memory_order_relaxed);
    }

    void dec_ref()
    {
        if (!ctrl_) return;
        if (ctrl_->ref_count.fetch_sub(1, std::memory_order_acq_rel) == 1) {
            // 计数归零,释放资源
            ctrl_->deleter(ptr_);
            delete ctrl_;
        }
    }

    T* ptr_;
    ControlBlock <T>* ctrl_;
};

2.3 线程安全性

  • 计数器使用 `std::atomic `,加、减操作分别采用 `fetch_add` / `fetch_sub`。
  • 读取计数使用 load(),保证可见性。
  • 由于所有操作都是原子性的,MySharedPtr 的复制、赋值、析构在多线程环境下都是安全的。

2.4 异常安全

  • 构造函数 MySharedPtr(T*, deleter) 只在 new ControlBlock 成功后才会持有指针;若 new 抛异常,资源已被 ptr_ 拥有,但不需要显式释放,编译器会自动析构 ptr_
  • 赋值操作采用“copy-and-swap”惯用法,确保在抛异常时不破坏原对象状态。

3. 使用示例

3.1 共享普通对象

int main()
{
    MySharedPtr <int> p1(new int(42));
    std::cout << "use_count p1: " << p1.use_count() << '\n'; // 1

    MySharedPtr <int> p2 = p1; // 共享所有权
    std::cout << "use_count p1: " << p1.use_count() << '\n'; // 2

    p2.reset();
    std::cout << "use_count p1 after reset p2: " << p1.use_count() << '\n'; // 1
}

3.2 使用自定义删除器

struct FileHandle {
    FILE* fp;
};

void close_file(FileHandle* fh)
{
    if (fh->fp) fclose(fh->fp);
    delete fh;
}

int main()
{
    FileHandle* fh = new FileHandle{fopen("log.txt", "w")};
    MySharedPtr <FileHandle> sp(fh, close_file);
    // ...
} // sp析构时会调用 close_file

3.3 多线程安全

void worker(MySharedPtr <int> ptr)
{
    for (int i = 0; i < 1000; ++i)
        std::cout << *ptr << '\n';
}

int main()
{
    MySharedPtr <int> shared(new int(10));
    std::thread t1(worker, shared);
    std::thread t2(worker, shared);
    t1.join(); t2.join(); // 所有线程共享同一对象,计数安全
}

4. 进一步扩展

功能 说明
弱引用(WeakPtr) std::weak_ptr 相似,提供 use_count()expired()lock()
多继承与偏移 通过模板特化支持 static_castdynamic_cast 的偏移计算。
自增自减模板 MySharedPtr 提供 operator++/-- 用于计数操作(仅用于学习演示)。
内存池与自定义分配器 ControlBlock 或对象分配时使用自定义内存池,降低分配开销。

5. 结语

通过本文的实现,您已经了解了如何在 C++ 中从零开始构建一个功能完整且线程安全的共享智能指针。虽然 std::shared_ptr 已经足够强大,但自定义实现可以让您在满足特殊需求(如自定义删除器、非标准分配器、调试追踪等)时拥有更大的灵活性。希望这篇文章能为您在深入理解 C++ 内存管理机制的道路上提供一点帮助。

发表评论