1. 项目背景与核心价值
在C++开发中,内存管理一直是让开发者又爱又恨的话题。原生指针用起来灵活高效,但稍不留神就会导致内存泄漏、悬垂指针等问题。标准库提供的shared_ptr虽然好用,但理解其底层实现原理对提升编程功力至关重要。这就是为什么我们需要亲手实现一个简化版的共享智能指针——my_shared_ptr。
这个项目不仅是一个教学演示,更是一个深入理解现代C++内存管理机制的绝佳实践。通过实现引用计数、自定义删除器等核心功能,你会对以下概念有更透彻的认识:
- 资源获取即初始化(RAII)原则
- 控制块(control block)设计模式
- 线程安全的引用计数策略
- 类型擦除(type erasure)在删除器中的应用
2. 核心设计解析
2.1 引用计数机制
引用计数是共享指针的核心,我们的实现采用经典的控制块设计:
cpp复制template <typename T>
class my_shared_ptr {
private:
T* ptr; // 原始指针
size_t* ref_count; // 引用计数器
Deleter* deleter; // 删除器接口
struct ControlBlock {
size_t count;
Deleter* deleter;
};
};
引用计数的增减遵循三条黄金规则:
- 构造函数分配新资源时,计数器初始化为1
- 拷贝构造函数/赋值操作符递增源对象的计数器
- 析构函数递减计数器,当计数归零时调用删除器释放资源
注意:原始实现中计数器使用
size_t*是为了与多个shared_ptr实例共享计数。更工程化的实现会将其封装在独立的ControlBlock类中。
2.2 删除器设计
标准库的shared_ptr支持自定义删除器,我们的实现也采用类似策略:
cpp复制struct Deleter {
virtual void operator()(void*) = 0;
virtual ~Deleter() = default;
};
template <typename T, typename D>
struct CustomDeleter : Deleter {
D d;
void operator()(void* p) override { d(static_cast<T*>(p)); }
};
这种基于虚函数的设计实现了类型擦除,允许用户传入任意可调用对象作为删除器,同时保持接口统一。例如可以这样使用:
cpp复制// 使用lambda作为删除器
my_shared_ptr<FILE> file(fopen("test.txt","r"),
[](FILE* f) { if(f) fclose(f); });
2.3 线程安全考量
虽然我们的教学实现未包含完整线程安全保证,但实际工程中需要考虑:
- 引用计数应使用原子操作(如
std::atomic<size_t>) - 控制块的分配/释放需要同步
- 指针的读写需要内存屏障
一个简单的线程安全计数器实现示例:
cpp复制class AtomicCounter {
std::atomic<size_t> count{1};
public:
size_t increment() { return ++count; }
size_t decrement() { return --count; }
size_t get() const { return count.load(); }
};
3. 完整实现代码
以下是my_shared_ptr的核心实现(省略部分辅助代码):
cpp复制template <typename T>
class my_shared_ptr {
T* ptr = nullptr;
ControlBlock* cb = nullptr;
void cleanup() {
if (!cb) return;
if (--cb->count == 0) {
cb->deleter->operator()(ptr);
delete cb;
}
ptr = nullptr;
cb = nullptr;
}
public:
// 默认构造函数
explicit my_shared_ptr(T* p = nullptr, Deleter* d = nullptr)
: ptr(p), cb(new ControlBlock{1, d ? d : new DefaultDeleter<T>()}) {}
// 拷贝构造
my_shared_ptr(const my_shared_ptr& other)
: ptr(other.ptr), cb(other.cb) {
if (cb) ++cb->count;
}
// 移动构造
my_shared_ptr(my_shared_ptr&& other) noexcept
: ptr(other.ptr), cb(other.cb) {
other.ptr = nullptr;
other.cb = nullptr;
}
// 析构函数
~my_shared_ptr() { cleanup(); }
// 赋值运算符
my_shared_ptr& operator=(const my_shared_ptr& rhs) {
if (this != &rhs) {
cleanup();
ptr = rhs.ptr;
cb = rhs.cb;
if (cb) ++cb->count;
}
return *this;
}
// 解引用运算符
T& operator*() const { return *ptr; }
T* operator->() const { return ptr; }
explicit operator bool() const { return ptr != nullptr; }
size_t use_count() const { return cb ? cb->count : 0; }
};
4. 关键问题与解决方案
4.1 循环引用问题
共享指针最著名的痛点就是循环引用。考虑以下场景:
cpp复制struct Node {
my_shared_ptr<Node> next;
};
auto node1 = my_shared_ptr<Node>(new Node());
auto node2 = my_shared_ptr<Node>(new Node());
node1->next = node2;
node2->next = node1; // 循环引用!
解决方案:
- 使用weak_ptr打破循环(我们的实现可扩展添加)
- 手动调用reset()断开引用
- 重新设计数据结构避免循环
4.2 自定义删除器的内存管理
删除器对象本身也需要正确管理生命周期。当前实现存在潜在内存泄漏:
cpp复制// 问题代码:删除器可能泄漏
my_shared_ptr<int> p(new int[10],
new CustomDeleter<int, std::default_delete<int[]>>());
改进方案:
- 在ControlBlock中直接存储删除器对象(而非指针)
- 使用std::function等类型擦除容器
- 实现删除器的共享所有权
4.3 异常安全保证
构造函数需要提供强异常安全保证:
cpp复制my_shared_ptr(T* p, Deleter* d = nullptr) {
try {
ptr = p;
cb = new ControlBlock{1, d ? d : new DefaultDeleter<T>()};
} catch (...) {
delete p;
delete d;
throw;
}
}
5. 性能优化技巧
5.1 小对象优化
对于小型对象,可以将控制块与对象内存合并分配:
cpp复制template <typename T, typename Deleter = DefaultDeleter<T>>
class compact_shared_ptr {
struct CombinedBlock : Deleter {
size_t count;
T object;
};
CombinedBlock* cb;
};
5.2 移动语义优化
移动操作应该零开销:
cpp复制my_shared_ptr(my_shared_ptr&& other) noexcept
: ptr(other.ptr), cb(other.cb) {
other.ptr = nullptr;
other.cb = nullptr; // 重要:确保源对象析构不会影响资源
}
5.3 引用计数缓存
频繁的原子操作影响性能,可采用延迟更新策略:
cpp复制class LazyCounter {
size_t local_count = 1;
std::atomic<size_t> global_count{1};
void sync() {
if (local_count > 0) {
global_count.fetch_add(local_count);
local_count = 0;
}
}
};
6. 测试用例设计
验证智能指针的正确性需要全面测试:
cpp复制void test_shared_ptr() {
// 基础功能测试
{
my_shared_ptr<int> p1(new int(42));
assert(*p1 == 42 && p1.use_count() == 1);
auto p2 = p1;
assert(p1.use_count() == 2);
}
// 删除器测试
{
bool deleted = false;
my_shared_ptr<int> p(new int, [&](int* p) { delete p; deleted = true; });
p.reset();
assert(deleted);
}
// 异常安全测试
try {
throw std::runtime_error("test");
my_shared_ptr<int> p(new int);
} catch (...) {
// 确保没有内存泄漏
}
}
7. 工程实践建议
- 与标准库兼容:实现get()、reset()等标准接口,方便替换
- 类型转换支持:添加dynamic_pointer_cast等转换函数
- 调试支持:重载operator<<方便日志输出
- 自定义分配器:扩展支持内存池等高级分配策略
一个实用的调试辅助函数示例:
cpp复制template <typename T>
std::ostream& operator<<(std::ostream& os, const my_shared_ptr<T>& p) {
return os << "my_shared_ptr(" << p.get() << ", use_count=" << p.use_count() << ")";
}
实现共享智能指针是理解现代C++内存管理的最佳实践之一。通过这个项目,你不仅掌握了智能指针的实现原理,更重要的是培养了资源管理的思维方式。在真实项目中,建议优先使用标准库的shared_ptr,但了解其底层机制能让你在遇到复杂内存问题时游刃有余。