老生常谈之引用计数:《More Effective C++》条款29
《More Effective C++》条款29深入探讨了**引用计数(Reference Counting)这一内存管理技术,通过共享对象实现优化性能,并结合写时拷贝(Copy-On-Write)**机制解决资源复用与修改安全的矛盾。以下是条款29的核心内容与实践要点:
一、引用计数的核心原理
引用计数通过跟踪对象被引用的次数,实现资源的自动释放。当最后一个引用离开作用域时,对象被销毁。条款29以String
类为例,将字符串值封装在StringValue
类中,并维护一个refCount
计数器:
- 共享机制:多个
String
对象可共享同一StringValue
实例,减少内存分配与拷贝开销。 - 自动释放:当
refCount
降为0时,StringValue
自动销毁,避免内存泄漏。
二、写时拷贝(Copy-On-Write)的实现
为了在共享与修改之间平衡,条款29引入写时拷贝技术:
- 读操作优化:
const operator()
直接返回共享值的引用,无需拷贝。 - 写操作分离:
non-const operator()
在修改前检查引用计数:
此逻辑确保只有在实际修改时才创建独立副本,显著提升性能。char& String::operator()(int index) {if (value->refCount > 1) { // 存在共享时--value->refCount; // 减少原引用计数value = new StringValue(value->data); // 创建独立拷贝}return value->data(index); // 返回新拷贝的引用 }
三、引用计数基类(RCObject)的设计
条款29提出通过抽象基类RCObject封装引用计数的通用逻辑,使StringValue
等类可继承复用:
class RCObject {
public:void addReference() { ++refCount; }void removeReference() { if (--refCount == 0) delete this; }bool isShared() const { return refCount > 1; }bool isShareable() const { return shareable; }void markUnshareable() { shareable = false; }
private:int refCount;bool shareable; // 标记是否允许共享
};
- 共享控制:
shareable
标志防止不可变对象被共享,避免多线程或外部指针导致的数据竞争。 - 安全性:
removeReference
在引用计数为0时delete this
,要求RCObject
子类必须在堆上分配。
四、智能指针RCPtr的封装
为了简化引用计数的手动管理,条款29设计了智能指针模板RCPtr,自动处理计数增减:
template<typename T>
class RCPtr {
public:RCPtr(T* realPtr = 0) : pointee(realPtr) { init(); }RCPtr(const RCPtr& rhs) : pointee(rhs.pointee) { init(); }~RCPtr() { if (pointee) pointee->removeReference(); }private:T* pointee;void init() {if (pointee && !pointee->isShareable()) {pointee = new T(*pointee); // 不可共享时深拷贝}if (pointee) pointee->addReference(); // 增加引用计数}
};
- 自动拷贝策略:若
StringValue
不可共享,RCPtr
在构造时自动深拷贝,确保线程安全。 - 异常安全:通过RAII机制,确保在异常抛出时引用计数正确释放。
五、关键挑战与解决方案
-
外部指针/引用的干扰:
- 问题:若用户保存
operator()
返回的char&
,后续修改可能影响所有共享该值的String
对象。 - 解决:
- 文档声明:明确禁止保存临时引用。
- 共享标志:通过
markUnshareable()
标记不可共享的值对象,强制后续修改创建独立拷贝。 - Proxy类(条款M30):区分读写操作,精确控制共享行为。
- 问题:若用户保存
-
线程安全:
- 单线程场景:条款29默认假设单线程环境,引用计数操作无需同步。
- 多线程扩展:实际应用中需结合互斥锁(如
std::mutex
)保护refCount
,但条款未深入讨论。
-
循环引用:
- 条款29的处理:通过
shareable
标志限制共享范围,避免对象间永久互相引用。 - 现代方案:C++11的
weak_ptr
可更优雅地解决循环引用,但条款29发布时(1996年)尚未引入。
- 条款29的处理:通过
六、引用计数的优缺点
优点 | 缺点 |
---|---|
自动内存管理,避免泄漏 | 循环引用需手动处理(条款29通过共享标志缓解) |
写时拷贝显著减少不必要的拷贝操作 | 多线程环境需额外同步机制 |
可精确控制对象生命周期 | 引用计数操作带来轻微性能开销 |
七、总结
条款29通过String
类的设计,系统展示了引用计数与写时拷贝的实现细节,强调了以下最佳实践:
- 分层设计:通过
RCObject
基类与RCPtr
智能指针分离引用计数逻辑与业务逻辑。 - 延迟拷贝:仅在实际修改时创建副本,平衡效率与安全性。
- 明确接口约束:通过
shareable
标志与文档声明,限制潜在风险操作。
引用计数至今仍是C++中高效管理共享资源的重要手段,条款29的思想为理解std::shared_ptr
等现代智能指针提供了底层实现视角。