19. 手写一个简易 shared_ptr:引用计数类、线程安全递增递减、析构逻辑

说实话,shared_ptr 是 C++ 里我最常用的智能指针之一。但用了这么多年,你真的理解它底层的引用计数是怎么工作的吗?

我记得刚入行那会儿,有个同事在项目里自己封装了一个“智能指针”,结果线上服务跑着跑着就崩了。查了半天,发现是引用计数在多线程环境下被写乱了。嗯,从那以后我就养成了一个习惯——凡是涉及共享所有权,必须搞清楚引用计数的线程安全

今天我们就手写一个简易的 shared_ptr。不追求完美,但要把核心逻辑讲透。

核心设计思路

一个 shared_ptr 说白了就两样东西:

  • 一个指向原始对象的指针
  • 一个指向引用计数的指针

引用计数是个整数,记录有多少个 shared_ptr 共享同一个对象。当计数归零时,就释放对象。

你想想看,如果每个 shared_ptr 都自己存一份计数,那还怎么共享?所以计数必须放在堆上,所有实例共享同一份。

关键点:引用计数对象本身也需要动态分配,并且要保证线程安全。

引用计数类

我们先写一个引用计数类。它只做一件事:管理一个整数,并提供原子操作。

#include <atomic>

class RefCount {
public:
    explicit RefCount(int initial = 1) 
        : count_(initial) {}

    // 递增,返回新值
    int increment() {
        return ++count_;
    }

    // 递减,返回新值
    int decrement() {
        return --count_;
    }

    int get() const {
        return count_.load();
    }

private:
    std::atomic<int> count_;
};

这里我用了 std::atomic<int>。为什么不用普通的 int

我在项目中遇到过这样一个坑:两个线程同时对一个 int++ 操作,结果只加了 1 次。因为 ++ 不是原子操作,它分“读-改-写”三步。两个线程同时读到了同一个值,各自加 1 再写回去,就覆盖了对方的修改。

std::atomic 就能保证 ++-- 是原子的。说白了,就是硬件级别帮你加锁了,性能比用 mutex 好得多。

线程安全递增递减

刚才的 RefCount 类已经用了原子操作,所以递增递减本身就是线程安全的。但这里有个细节:递减后判断是否为零,这个“判断”动作也必须和“递减”绑定在一起,不能分开。

举个例子:

// 错误写法
if (ref_count.decrement() == 0) {
    delete object;
}

这段代码有问题吗?

有。假设两个线程同时调用 decrement(),都返回了 0。然后两个线程都去 delete object,double free 就来了。

正确的做法是:只有最后一个递减的线程负责释放资源。因为原子操作保证了只有一个线程能看到计数变为 0。

注意:递减操作本身是原子的,但“递减+判断”这个组合操作,必须由同一个线程连续完成,中间不能被其他线程打断。好在 std::atomic-- 操作返回的是修改后的值,所以判断是安全的。

析构逻辑

析构逻辑其实就一句话:递减引用计数,如果归零则释放对象和计数本身

但这里有个容易忽略的点:shared_ptr 管理的是两个资源——原始对象和引用计数对象。两者都需要释放。

~shared_ptr() {
    if (ref_count_) {
        if (ref_count_->decrement() == 0) {
            delete ptr_;          // 释放原始对象
            delete ref_count_;    // 释放引用计数
        }
    }
}

我个人习惯把析构逻辑封装成一个辅助函数,这样代码更清晰:

void release() {
    if (ref_count_ && ref_count_->decrement() == 0) {
        delete ptr_;
        delete ref_count_;
        ptr_ = nullptr;
        ref_count_ = nullptr;
    }
}

完整实现

下面是一个简易 shared_ptr 的完整代码。我刻意省略了 make_sharedweak_ptr 等高级功能,只保留核心逻辑。

#include <atomic>
#include <utility>

class RefCount {
public:
    explicit RefCount(int initial = 1) : count_(initial) {}
    int increment() { return ++count_; }
    int decrement() { return --count_; }
    int get() const { return count_.load(); }
private:
    std::atomic<int> count_;
};

template<typename T>
class shared_ptr {
public:
    // 默认构造
    shared_ptr() : ptr_(nullptr), ref_count_(nullptr) {}

    // 从原始指针构造
    explicit shared_ptr(T* ptr) 
        : ptr_(ptr), ref_count_(new RefCount()) {}

    // 拷贝构造
    shared_ptr(const shared_ptr& other) 
        : ptr_(other.ptr_), ref_count_(other.ref_count_) {
        if (ref_count_) {
            ref_count_->increment();
        }
    }

    // 拷贝赋值
    shared_ptr& operator=(const shared_ptr& other) {
        if (this != &other) {
            release();  // 释放当前资源
            ptr_ = other.ptr_;
            ref_count_ = other.ref_count_;
            if (ref_count_) {
                ref_count_->increment();
            }
        }
        return *this;
    }

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

    // 移动赋值
    shared_ptr& operator=(shared_ptr&& other) noexcept {
        if (this != &other) {
            release();
            ptr_ = other.ptr_;
            ref_count_ = other.ref_count_;
            other.ptr_ = nullptr;
            other.ref_count_ = nullptr;
        }
        return *this;
    }

    // 析构
    ~shared_ptr() {
        release();
    }

    // 基本接口
    T* get() const { return ptr_; }
    T& operator*() const { return *ptr_; }
    T* operator->() const { return ptr_; }
    int use_count() const { return ref_count_ ? ref_count_->get() : 0; }
    explicit operator bool() const { return ptr_ != nullptr; }

private:
    T* ptr_;
    RefCount* ref_count_;

    void release() {
        if (ref_count_ && ref_count_->decrement() == 0) {
            delete ptr_;
            delete ref_count_;
        }
        ptr_ = nullptr;
        ref_count_ = nullptr;
    }
};

引用计数生命周期图

下面这张图展示了两个 shared_ptr 共享同一个对象时,引用计数的变化过程:

shared_ptr 引用计数生命周期 阶段1:创建 sp1 sp1 RefCount count = 1 原始对象 阶段2:拷贝构造 sp2(sp1) sp1 RefCount count = 2 原始对象 sp2 阶段3:sp2 析构(count 减为 1) sp1 RefCount count = 1 原始对象

避坑指南

我曾经在代码里犯过一个低级错误:在拷贝赋值操作符里,先 release() 再拷贝指针。结果如果 otherthis 指向同一个对象,release() 直接把对象释放了,后面拷贝的就是野指针。

所以正确的顺序是:先递增 other 的计数,再释放当前资源。这就是为什么标准库的 shared_ptr 赋值操作符里有个“先增后减”的惯用法。

小技巧:如果你不想自己处理这些边界情况,直接用 std::shared_ptr 就好。手写只是为了理解原理,生产环境还是用标准库更稳妥。

总结

手写 shared_ptr 的核心就三点:

  • 引用计数必须放在堆上,所有实例共享
  • 递增递减必须是原子操作,否则多线程下会出问题
  • 析构时只有最后一个线程负责释放,判断条件就是递减后计数为 0

嗯,其实标准库的实现比这个复杂得多。它还要考虑 weak_ptrmake_shared 的内存优化、自定义删除器等等。但核心逻辑,就是我们今天写的这些。

下次面试如果被问到 shared_ptr 的实现,你可以从引用计数讲起,再聊到线程安全,最后提一嘴析构的竞态条件。相信我,面试官会对你刮目相看的。


公众号:蓝海资料掘金营,微信deep3321