27. 侵入式智能指针:当引用计数存在于对象内部时

讲完了 shared_ptrweak_ptr,我们来看一个不太一样的设计思路。说实话,我第一次接触侵入式智能指针时,心里是有点抗拒的——「引用计数怎么能放在对象内部呢?这不就把内存管理的逻辑和业务逻辑耦合在一起了吗?」

后来在做一个嵌入式项目时,我才真正理解了它的价值。当时内存非常有限,每个字节都得精打细算。标准 shared_ptr 的 control block 开销让我很头疼。嗯,这时候侵入式智能指针就派上用场了。

什么是侵入式智能指针?

简单说,侵入式智能指针要求对象自己管理引用计数。引用计数不是放在外部的 control block 里,而是作为对象的一个成员变量存在。

你想想看,这和 shared_ptr 最大的区别在哪?

  • shared_ptr:引用计数在堆上单独分配,对象不知道自己在被引用
  • 侵入式指针:引用计数在对象内部,对象知道自己被引用了多少次

说白了,侵入式智能指针把「谁负责计数」这个问题从外部转移到了内部。对象自己管自己的引用计数。

为什么需要它?

我在项目中遇到过几个场景,用 shared_ptr 很别扭,但侵入式指针却很自然:

  1. 内存极度受限的环境:嵌入式系统、游戏引擎。多一次堆分配都是浪费
  2. 需要从原始指针构造智能指针:有些遗留代码返回裸指针,你又不想大改
  3. 对象生命周期由外部框架管理:比如 COM 组件、Qt 的 QObject

核心区别一句话总结shared_ptr 的引用计数在对象外面,侵入式指针的引用计数在对象里面。

侵入式指针的工作原理

侵入式智能指针通常需要两个核心函数:intrusive_ptr_add_refintrusive_ptr_release。这两个函数由你来实现,智能指针模板会调用它们。

来看一个最简单的例子:

#include <boost/intrusive_ptr.hpp>
#include <atomic>

class MyObject {
public:
    MyObject() : ref_count_(0) {}
    
    // 友元函数,让 intrusive_ptr 能访问
    friend void intrusive_ptr_add_ref(MyObject* p) {
        p->ref_count_.fetch_add(1, std::memory_order_relaxed);
    }
    
    friend void intrusive_ptr_release(MyObject* p) {
        if (p->ref_count_.fetch_sub(1, std::memory_order_acq_rel) == 1) {
            delete p;
        }
    }
    
private:
    std::atomic<int> ref_count_;  // 引用计数在对象内部
};

int main() {
    boost::intrusive_ptr<MyObject> p1(new MyObject());
    boost::intrusive_ptr<MyObject> p2 = p1;  // 引用计数变成 2
    // 离开作用域时,引用计数递减,直到 0 时自动 delete
    return 0;
}

看到没?ref_count_MyObject 的成员变量。每个对象都自带一个计数器。

侵入式 vs 非侵入式:对比表格

对比维度 侵入式 (intrusive_ptr) 非侵入式 (shared_ptr)
引用计数位置 对象内部 外部 control block
额外内存开销 每个对象多一个 int 一次 control block 堆分配
从裸指针构造 安全,直接使用 危险,容易 double delete
对对象的要求 必须继承或包含计数 无要求,任何类型都可以
性能 少一次间接访问,更快 多一次指针跳转
适用场景 嵌入式、游戏、COM 通用场景

侵入式指针的典型应用:COM 组件

如果你用过 Windows 开发,一定见过 COM 的 AddRefRelease。这其实就是侵入式引用计数的经典实现。

class IUnknown {
public:
    virtual ULONG AddRef() = 0;
    virtual ULONG Release() = 0;
};

class MyComObject : public IUnknown {
public:
    MyComObject() : ref_count_(1) {}  // 创建时引用计数为 1
    
    ULONG AddRef() override {
        return ++ref_count_;
    }
    
    ULONG Release() override {
        ULONG ret = --ref_count_;
        if (ret == 0) {
            delete this;  // 自己删除自己
        }
        return ret;
    }
    
private:
    ULONG ref_count_;
};

嗯,这里要注意:delete this 在 COM 里是合法的,但前提是你得确保对象是在堆上分配的。我在项目中见过有人把 COM 对象分配在栈上,结果 Release 时直接崩溃——血的教训。

侵入式指针的优缺点

我个人习惯在以下场景优先考虑侵入式指针:

  • 对象数量巨大:每个对象省一次堆分配,积少成多
  • 需要频繁从裸指针构造智能指针:侵入式指针不怕多个智能指针管理同一个对象
  • 对象生命周期明确:你知道对象一定在堆上,且不会被栈分配

但缺点也很明显:

  • 污染对象定义:每个类都得加一个引用计数成员
  • 不能用于第三方库的类:你没法给 std::string 加个计数器
  • 容易忘记实现 add_refrelease:编译能过,运行会崩

避坑指南:我曾经在一个项目里,团队有人忘了给新加的类实现 intrusive_ptr_release,结果引用计数永远不会归零,内存泄漏了整整一周才发现。所以我的建议是:要么用宏强制所有侵入式类都实现这两个函数,要么写个单元测试统一检查。

什么时候该用侵入式指针?

说实话,大部分 C++ 项目用 shared_ptr 就够了。但如果你遇到以下情况,不妨考虑侵入式:

  1. 性能敏感:减少一次间接访问和一次堆分配
  2. 对象生命周期需要被外部代码管理:比如插件系统、脚本绑定
  3. 你已经在用类似 COM 的模式:那就别折腾了,直接用侵入式

我的个人经验:如果你不确定该用哪种,先用 shared_ptr。等性能分析告诉你问题出在 control block 上时,再考虑换成侵入式。不要过早优化。

侵入式指针的知识体系

下面这张图帮你理清侵入式智能指针在整个内存管理中的位置:

侵入式智能指针知识体系 C++ 智能指针 非侵入式 (shared_ptr/weak_ptr) 侵入式 (intrusive_ptr) 核心特征 引用计数在对象内部 需要实现 add_ref / release 典型应用场景 嵌入式系统 · 游戏引擎 · COM组件 · 插件架构 与非侵入式对比 • 侵入式:少一次堆分配,更快 • 非侵入式:通用性强,无侵入 • 侵入式:对象必须包含计数 • 非侵入式:任何类型都可用 • 侵入式:从裸指针构造安全 • 非侵入式:从裸指针构造危险

最后说两句

侵入式智能指针不是银弹,它解决的是特定场景下的问题。我个人觉得,理解它的设计思想比记住 API 更重要——它告诉我们,内存管理策略可以灵活选择,没有绝对的对错。

嗯,如果你在工作中遇到了那种「用 shared_ptr 总觉得哪里不对劲」的情况,不妨想想侵入式方案。说不定它就是你要找的答案。


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