在 C++11 之前,管理动态分配的资源主要靠手写的 delete,这很容易导致内存泄漏、野指针等问题。C++11 开始引入了标准智能指针 std::unique_ptr、std::shared_ptr、std::weak_ptr,极大简化了资源管理。下面我们以 std::shared_ptr 为例,探讨自己实现一个共享指针时需要关注的核心设计与实现细节,帮助读者深入理解其工作机制。
1. 共享计数的基本思路
std::shared_ptr 的核心是引用计数。每个共享指针实例都持有一份对同一对象的引用计数,只有当计数降到 0 时才真正释放对象。实现共享计数通常采用一个独立的计数器对象(如 std::atomic<std::size_t>),或者直接将计数器放在一个控制块(control block)里。
template <typename T>
class SharedPtr {
private:
T* ptr; // 实际指向的对象
std::size_t* refCount; // 引用计数
// ...
};
关键在于 计数的原子性:多线程环境下,计数器的加减操作必须是线程安全的。常见做法是使用 std::atomic<std::size_t> 或者在每个 SharedPtr 的复制/移动操作时手动锁住计数器。
2. 构造与析构
2.1 默认构造
默认构造不指向任何对象,计数器为 nullptr。
SharedPtr() : ptr(nullptr), refCount(nullptr) {}
2.2 从裸指针构造
直接使用裸指针时,需要为计数器分配空间,并初始化为 1。
explicit SharedPtr(T* p) : ptr(p) {
refCount = new std::size_t(1);
}
2.3 拷贝构造
拷贝构造时,需要把指针和计数器复制过来,并对计数器递增。
SharedPtr(const SharedPtr& other) : ptr(other.ptr), refCount(other.refCount) {
if (refCount) ++(*refCount);
}
2.4 移动构造
移动构造时,将资源所有权转移给新对象,源对象置为空。
SharedPtr(SharedPtr&& other) noexcept : ptr(other.ptr), refCount(other.refCount) {
other.ptr = nullptr;
other.refCount = nullptr;
}
2.5 析构
析构时递减计数器,并在计数为 0 时删除指针和计数器。
~SharedPtr() {
release();
}
void release() {
if (refCount && --(*refCount) == 0) {
delete ptr;
delete refCount;
}
}
3. 赋值操作
3.1 拷贝赋值
先递减自身计数,再复制别人的指针与计数器,最后递增新计数器。
SharedPtr& operator=(const SharedPtr& other) {
if (this != &other) {
release(); // 先释放旧资源
ptr = other.ptr;
refCount = other.refCount;
if (refCount) ++(*refCount);
}
return *this;
}
3.2 移动赋值
先释放旧资源,然后转移指针和计数器。
SharedPtr& operator=(SharedPtr&& other) noexcept {
if (this != &other) {
release();
ptr = other.ptr;
refCount = other.refCount;
other.ptr = nullptr;
other.refCount = nullptr;
}
return *this;
}
4. 访问与操作
operator*与operator->:提供对所管理对象的访问。use_count():返回当前引用计数(如果计数器为空返回 0)。unique():当计数为 1 时返回 true。
T& operator*() const { return *ptr; }
T* operator->() const { return ptr; }
std::size_t use_count() const { return refCount ? *refCount : 0; }
bool unique() const { return use_count() == 1; }
5. 线程安全细节
如果你想让 SharedPtr 在多线程中安全使用,最简单的做法是把 refCount 定义为 std::atomic<std::size_t>:
std::atomic<std::size_t>* refCount;
然后所有的加/减计数操作都使用 ++(*refCount) 与 --(*refCount)。
注意:--(*refCount) 的返回值不一定是新的计数,需要先递减后判断是否为 0。
6. 控制块(Control Block)改进
上面示例使用了两个独立的动态分配对象(ptr 与 refCount)。实际实现中,C++ 标准库通常采用一个 控制块(ControlBlock)来同时存储指针、计数、以及可选的自定义删除器。
struct ControlBlock {
T* ptr;
std::atomic<std::size_t> count;
// 可选自定义删除器
std::function<void(T*)> deleter;
};
SharedPtr 只持有指向 ControlBlock 的指针。这样可以在需要时支持 自定义删除器、弱引用(weak_ptr)等高级功能。
7. 完整代码(简化版)
#include <atomic>
#include <cstddef>
#include <functional>
template <typename T>
class SharedPtr {
private:
struct ControlBlock {
T* ptr;
std::atomic<std::size_t> count;
std::function<void(T*)> deleter;
ControlBlock(T* p)
: ptr(p), count(1), deleter([](T* p){ delete p; }) {}
};
ControlBlock* cb;
void release() {
if (cb && --cb->count == 0) {
cb->deleter(cb->ptr);
delete cb;
}
}
public:
// 默认构造
SharedPtr() : cb(nullptr) {}
// 从裸指针构造
explicit SharedPtr(T* p) : cb(new ControlBlock(p)) {}
// 拷贝构造
SharedPtr(const SharedPtr& other) : cb(other.cb) {
if (cb) ++cb->count;
}
// 移动构造
SharedPtr(SharedPtr&& other) noexcept : cb(other.cb) {
other.cb = nullptr;
}
// 析构
~SharedPtr() { release(); }
// 拷贝赋值
SharedPtr& operator=(const SharedPtr& other) {
if (this != &other) {
release();
cb = other.cb;
if (cb) ++cb->count;
}
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; }
// 信息
std::size_t use_count() const { return cb ? cb->count : 0; }
bool unique() const { return use_count() == 1; }
};
8. 小结
- 引用计数是实现
shared_ptr的核心,需保证线程安全。 - 控制块是实现自定义删除器、弱引用的关键结构。
- 拷贝/移动语义需仔细处理计数递增/递减和资源转移。
- 通过上述实现,可以更好地理解标准库
std::shared_ptr的工作机制,为后续学习std::weak_ptr、std::enable_shared_from_this等高级特性打下坚实基础。
希望这篇文章能帮助你从底层实现角度把握共享指针的设计与实现,为日后的 C++ 代码写作提供更深的技术支撑。