商城首页欢迎来到中国正版软件门户

您的位置:首页 >C++实现简单的垃圾回收RAII方案 _ 计数指针原理【源码】

C++实现简单的垃圾回收RAII方案 _ 计数指针原理【源码】

  发布于2026-05-03 阅读(0)

扫一扫,手机访问

C++实现简单的垃圾回收RAII方案 _ 计数指针原理【源码】

C++实现简单的垃圾回收RAII方案 _ 计数指针原理【源码】

为什么 raw_ptr + delete 容易出 double-free 或 dangling pointer

说到底,裸指针手动管理生命周期,本质上就是把“谁该释放”和“何时释放”的决策权,完全交给了程序员。一旦对象被多个模块共享,或者程序执行中途因为异常跳出作用域,那个关键的delete就很容易被漏掉、被重复调用,甚至在对象早已销毁后,代码还在尝试解引用它。这里有个常见的误解:RAII本身并不解决共享所有权的问题,它只保证在单个作用域内资源能自动释放。而像std::shared_ptr这样的计数指针,才是对RAII理念的自然延伸——它把“引用计数”这个状态本身,也纳入了资源管理的范畴。

实际开发中,下面这些错误现象屡见不鲜:

  • 函数返回了一个在局部new出来的对象指针,调用方却忘了delete,结果就是内存泄漏。
  • 两个std::shared_ptr虽然指向同一块原始内存,但却是分别用new构造的(非同源),导致引用计数错乱,最终引发double-free。
  • 循环引用,比如A持有B的std::shared_ptr,B也持有A的,导致引用计数永远无法归零,对象无法被析构。

如何手写一个最小可用的 ref_ptr(非线程安全版)

要实现一个最简可用的引用计数指针,核心离不开三要素:指向对象的原始指针、一个独立的引用计数器、以及封装在析构函数里的资源释放逻辑。这里有几个关键点需要注意:计数器不能放在对象内部(那是侵入式方案,不够通用),也不能和对象共用同一块内存(除非定制operator new),否则在delete对象时,计数器本身就无法被安全释放了。

具体操作上,有这么几个要点:

立即学习“C++免费学习笔记(深入)”;

  • 计数器必须动态分配:使用new size_t(1)来确保它的生命周期独立于它所托管的对象。
  • 拷贝构造与赋值是关键:执行拷贝时,要先对原指针的计数器执行--(*cnt),然后检查计数是否归零,以此来决定是否需要delete托管对象和计数器本身。
  • 移动语义需显式置空:实现移动操作时,一定要记得将右值(被移动对象)的指针和计数器置空,防止后续析构函数误删资源。
  • 接口行为需一致:提供get()operator->等接口时,其行为应当与std::shared_ptr保持一致,降低使用者的心智负担。

下面是一个简化版的关键路径代码示例:

template
class ref_ptr {
    T* ptr_;
    size_t* cnt_;
public:
    explicit ref_ptr(T* p = nullptr) : ptr_(p), cnt_(p ? new size_t(1) : nullptr) {}

    ref_ptr(const ref_ptr& other) : ptr_(other.ptr_), cnt_(other.cnt_) {
        if (cnt_) ++(*cnt_);
    }

    ~ref_ptr() {
        if (cnt_ && --(*cnt_) == 0) {
            delete ptr_;
            delete cnt_;
        }
    }

    T* get() const { return ptr_; }
    T& operator*() const { return *ptr_; }
    T* operator->() const { return ptr_; }
};

ref_ptrstd::shared_ptr 在 reset / release 行为上的关键差异

标准库的std::shared_ptr::reset()设计得很周全:它会先减少旧资源的引用计数,然后再去接管新对象。而手写版本如果没实现类似的逻辑,直接进行ptr_ = new_obj; cnt_ = new_cnt;这样的赋值,就会跳过对旧资源的清理步骤,从而导致内存泄漏或悬垂指针。

在实现这些功能时,有几个坑特别容易踩到:

  • 用赋值替代reset():如果没实现专门的reset()方法,靠拷贝赋值来替代,可能会触发不必要的拷贝构造和析构,不仅性能差,还会让引用计数的逻辑变得复杂和沉重。
  • 实现reset(nullptr)时忘记置空cnt_:这会导致后续析构函数仍然尝试去delete cnt_,访问野指针,引发未定义行为。
  • 误以为需要release():对于计数指针来说,语义上并不支持“交出所有权但不减少引用计数”这种操作,这会破坏RAII的基本契约,所以通常不应该提供release()方法。
  • 缺乏自定义删除器支持:简易实现往往不支持自定义删除器(Deleter),因此无法处理那些由malloc分配,或者需要调用CloseHandle等特定函数来释放的资源。

循环引用真的只能靠 std::weak_ptr 解?手写怎么加

弱引用(weak_ptr)并不是“另一个拥有所有权的指针”,它本质上是对同一块控制块(包含引用计数器)的**非拥有式观察者**。它不参与强引用计数(cnt_)的增减,只在需要时尝试“升级”为强引用(即检查对象是否还存活)。因此,要实现弱引用,就必须在控制块里额外维护一个“弱计数”字段,用来记录有多少个weak_ptr正在观察这个计数器。

自己动手实现的话,必须把握住这几个要点:

  • 扩展计数器结构:计数器需要从单个size_t扩展为至少包含strong_countweak_count两个字段的结构体。
  • 析构逻辑分离weak_ptr析构时,只减少weak_count;只有当strong_count == 0 && weak_count == 0两个条件同时满足时,才能安全地释放计数器内存本身。
  • 安全地“升级”lock()方法需要原子性地读取strong_count并判断是否大于0,只有在对象存活时,才构造一个新的ref_ptr(此时才会增加strong_count)。
  • 明确并发限制:即使你实现的是非线程安全版本,也必须在代码中明确标注“不保证多线程并发调用的安全性”,防止使用者误将其用于异步场景,导致难以调试的问题。

走到这一步,其复杂度已经逼近std::shared_ptr实现的下限了。对于绝大多数项目而言,直接使用标准库是更稳健的选择。自己实现引用计数指针,通常目的仅限于深入理解其原理,或者是为了嵌入那些无法使用STL的受限环境,而不是为了替代标准库。

本文转载于:https://www.php.cn/faq/2313308.html 如有侵犯,请联系zhengruancom@outlook.com删除。
免责声明:正软商城发布此文仅为传递信息,不代表正软商城认同其观点或证实其描述。

热门关注