27. 侵入式智能指针:当引用计数存在于对象内部时
讲完了 shared_ptr 和 weak_ptr,我们来看一个不太一样的设计思路。说实话,我第一次接触侵入式智能指针时,心里是有点抗拒的——「引用计数怎么能放在对象内部呢?这不就把内存管理的逻辑和业务逻辑耦合在一起了吗?」
后来在做一个嵌入式项目时,我才真正理解了它的价值。当时内存非常有限,每个字节都得精打细算。标准 shared_ptr 的 control block 开销让我很头疼。嗯,这时候侵入式智能指针就派上用场了。
什么是侵入式智能指针?
简单说,侵入式智能指针要求对象自己管理引用计数。引用计数不是放在外部的 control block 里,而是作为对象的一个成员变量存在。
你想想看,这和 shared_ptr 最大的区别在哪?
- shared_ptr:引用计数在堆上单独分配,对象不知道自己在被引用
- 侵入式指针:引用计数在对象内部,对象知道自己被引用了多少次
说白了,侵入式智能指针把「谁负责计数」这个问题从外部转移到了内部。对象自己管自己的引用计数。
为什么需要它?
我在项目中遇到过几个场景,用 shared_ptr 很别扭,但侵入式指针却很自然:
- 内存极度受限的环境:嵌入式系统、游戏引擎。多一次堆分配都是浪费
- 需要从原始指针构造智能指针:有些遗留代码返回裸指针,你又不想大改
- 对象生命周期由外部框架管理:比如 COM 组件、Qt 的 QObject
核心区别一句话总结:shared_ptr 的引用计数在对象外面,侵入式指针的引用计数在对象里面。
侵入式指针的工作原理
侵入式智能指针通常需要两个核心函数:intrusive_ptr_add_ref 和 intrusive_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 的 AddRef 和 Release。这其实就是侵入式引用计数的经典实现。
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_ref和release:编译能过,运行会崩
避坑指南:我曾经在一个项目里,团队有人忘了给新加的类实现 intrusive_ptr_release,结果引用计数永远不会归零,内存泄漏了整整一周才发现。所以我的建议是:要么用宏强制所有侵入式类都实现这两个函数,要么写个单元测试统一检查。
什么时候该用侵入式指针?
说实话,大部分 C++ 项目用 shared_ptr 就够了。但如果你遇到以下情况,不妨考虑侵入式:
- 性能敏感:减少一次间接访问和一次堆分配
- 对象生命周期需要被外部代码管理:比如插件系统、脚本绑定
- 你已经在用类似 COM 的模式:那就别折腾了,直接用侵入式
我的个人经验:如果你不确定该用哪种,先用 shared_ptr。等性能分析告诉你问题出在 control block 上时,再考虑换成侵入式。不要过早优化。
侵入式指针的知识体系
下面这张图帮你理清侵入式智能指针在整个内存管理中的位置:
最后说两句
侵入式智能指针不是银弹,它解决的是特定场景下的问题。我个人觉得,理解它的设计思想比记住 API 更重要——它告诉我们,内存管理策略可以灵活选择,没有绝对的对错。
嗯,如果你在工作中遇到了那种「用 shared_ptr 总觉得哪里不对劲」的情况,不妨想想侵入式方案。说不定它就是你要找的答案。
公众号:蓝海资料掘金营,微信 deep3321