悠悠楠杉
手写简化版shared_ptr:深入理解C++引用计数智能指针
在C++开发中,内存管理一直是开发者必须面对的挑战。传统裸指针的显式delete
操作不仅容易导致内存泄漏,还可能引发悬垂指针等问题。本文将带你从零实现一个简化版的shared_ptr
,通过引用计数机制实现自动化内存管理。
一、引用计数基本原理
引用计数的核心思想是通过计数器跟踪资源被引用的次数,当计数归零时自动释放资源。这种机制需要解决三个关键问题:
- 计数器的存储位置(必须被所有引用共享)
- 线程安全性(本文示例暂不考虑)
- 循环引用问题(可通过weak_ptr解决,本文不涉及)
二、简化版SharedPtr实现
我们首先定义核心结构体ControlBlock
来保存引用计数:
cpp
template
struct ControlBlock {
T* ptr;
sizet refcount;
explicit ControlBlock(T* p) : ptr(p), ref_count(1) {}
~ControlBlock() {
delete ptr;
}
};
接下来实现SharedPtr
类模板:
cpp
template
class SharedPtr {
ControlBlock
// 辅助私有方法
void retain_() {
if (cb_) ++cb_->ref_count;
}
void release_() {
if (cb_ && --cb_->ref_count == 0) {
delete cb_;
}
}
public:
// 构造函数
explicit SharedPtr(T* ptr = nullptr)
: cb_(ptr ? new ControlBlock
// 拷贝构造
SharedPtr(const SharedPtr& other) : cb_(other.cb_) {
retain_();
}
// 移动构造
SharedPtr(SharedPtr&& other) noexcept : cb_(other.cb_) {
other.cb_ = nullptr;
}
// 析构函数
~SharedPtr() {
release_();
}
// 拷贝赋值
SharedPtr& operator=(const SharedPtr& other) {
if (this != &other) {
release_();
cb_ = other.cb_;
retain_();
}
return *this;
}
// 移动赋值
SharedPtr& operator=(SharedPtr&& other) noexcept {
if (this != &other) {
release_();
cb_ = other.cb_;
other.cb_ = nullptr;
}
return *this;
}
// 解引用操作符
T& operator*() const { return *cb_->ptr; }
T* operator->() const { return cb_->ptr; }
// 辅助方法
size_t use_count() const { return cb_ ? cb_->ref_count : 0; }
explicit operator bool() const { return cb_ && cb_->ptr; }
};
三、关键实现细节解析
控制块分离设计:
- 将引用计数与对象存储分离,避免侵入式设计
- 控制块的生命周期由所有
SharedPtr
实例共享管理
异常安全性:
- 构造函数确保要么完全成功,要么抛出异常前清理资源
- 赋值操作采用copy-and-swap惯用法保证强异常安全
资源释放时机:
- 当
ref_count
减到0时,自动调用控制块析构 - 析构函数中先
delete ptr
再释放控制块内存
- 当
四、使用示例与测试
cpp
class MyResource {
public:
MyResource() { std::cout << "Resource created\n"; }
~MyResource() { std::cout << "Resource destroyed\n"; }
void use() { std::cout << "Resource used\n"; }
};
void testfunc() {
SharedPtr
}
std::cout << "Use count: " << p1.use_count() << "\n"; // 输出1
} // p1析构时资源自动释放
五、与标准库shared_ptr的差异
- 缺少线程安全保证(标准库使用原子操作)
- 不支持自定义删除器
- 未实现weak_ptr相关功能
- 缺少类型转换接口(staticpointercast等)
- 简化了异常处理逻辑
六、扩展思考
- 循环引用问题:可通过实现weak_ptr打破循环,需要分离引用计数与对象生命周期
- 性能优化:考虑使用原子操作实现线程安全版本
- 内存分配策略:可优化控制块与对象的连续内存分配
最佳实践提示:生产环境建议直接使用std::shared_ptr,自定义实现主要用于学习原理和特殊场景定制。