在现代C++编程中,智能指针是管理动态资源的核心工具。标准库提供了std::shared_ptr、std::unique_ptr和std::weak_ptr三种主要类型,每种都有其适用场景。若想在项目中自定义智能指针,尤其是需要支持弱引用的场景,必须仔细设计计数机制和线程安全。以下是一种实现思路,涵盖了核心概念、关键代码示例以及常见陷阱。
1. 基本思路
- 引用计数:使用一个单独的计数器对象(类似于
std::shared_ptr的控制块)来记录强引用(strong_count)和弱引用(weak_count)。 - 控制块:在控制块中存放被管理对象的指针、
strong_count、weak_count以及可选的自定义删除器。 - 构造与析构:
MySharedPtr在构造时会增大strong_count,在析构时减小并在计数为0时销毁资源;MyWeakPtr仅操作weak_count,在释放时检查是否需要销毁控制块。 - 升级:
MyWeakPtr::lock()尝试将弱引用升级为强引用,若资源已被释放则返回空指针。
2. 核心数据结构
template <typename T>
struct ControlBlock {
T* ptr; // 被管理对象
std::atomic <size_t> strong{1}; // 强引用计数
std::atomic <size_t> weak{0}; // 弱引用计数
std::function<void(T*)> deleter; // 可选自定义删除器
ControlBlock(T* p, std::function<void(T*)> del = nullptr)
: ptr(p), deleter(del) {}
};
使用std::atomic保证多线程环境下计数操作的原子性。若你确定在单线程中使用,可以直接用size_t。
3. MySharedPtr实现
template <typename T>
class MySharedPtr {
public:
explicit MySharedPtr(T* p = nullptr)
: ctrl(p ? new ControlBlock <T>(p) : nullptr) {}
// 复制构造
MySharedPtr(const MySharedPtr& other) noexcept
: ctrl(other.ctrl) {
if (ctrl) ctrl->strong.fetch_add(1, std::memory_order_relaxed);
}
// 移动构造
MySharedPtr(MySharedPtr&& other) noexcept
: ctrl(other.ctrl) {
other.ctrl = nullptr;
}
~MySharedPtr() {
release();
}
MySharedPtr& operator=(const MySharedPtr& other) {
if (this != &other) {
release();
ctrl = other.ctrl;
if (ctrl) ctrl->strong.fetch_add(1, std::memory_order_relaxed);
}
return *this;
}
T* operator->() const { return ctrl->ptr; }
T& operator*() const { return *(ctrl->ptr); }
explicit operator bool() const { return ctrl && ctrl->ptr; }
size_t use_count() const {
return ctrl ? ctrl->strong.load(std::memory_order_relaxed) : 0;
}
private:
ControlBlock <T>* ctrl;
void release() {
if (ctrl && ctrl->strong.fetch_sub(1, std::memory_order_acq_rel) == 1) {
// 资源释放
if (ctrl->deleter)
ctrl->deleter(ctrl->ptr);
else
delete ctrl->ptr;
// 计数器已零,检查弱引用是否为零
if (ctrl->weak.load(std::memory_order_acquire) == 0)
delete ctrl;
}
}
};
4. MyWeakPtr实现
template <typename T>
class MyWeakPtr {
public:
explicit MyWeakPtr(const MySharedPtr <T>& shared)
: ctrl(shared.ctrl) {
if (ctrl) ctrl->weak.fetch_add(1, std::memory_order_relaxed);
}
MyWeakPtr(const MyWeakPtr& other) noexcept
: ctrl(other.ctrl) {
if (ctrl) ctrl->weak.fetch_add(1, std::memory_order_relaxed);
}
MyWeakPtr(MyWeakPtr&& other) noexcept
: ctrl(other.ctrl) {
other.ctrl = nullptr;
}
~MyWeakPtr() {
release();
}
MyWeakPtr& operator=(const MyWeakPtr& other) {
if (this != &other) {
release();
ctrl = other.ctrl;
if (ctrl) ctrl->weak.fetch_add(1, std::memory_order_relaxed);
}
return *this;
}
MySharedPtr <T> lock() const {
if (ctrl && ctrl->strong.load(std::memory_order_acquire) > 0) {
// 尝试提升计数
ctrl->strong.fetch_add(1, std::memory_order_relaxed);
return MySharedPtr <T>(ctrl);
}
return MySharedPtr <T>();
}
bool expired() const {
return !ctrl || ctrl->strong.load(std::memory_order_acquire) == 0;
}
private:
ControlBlock <T>* ctrl;
void release() {
if (ctrl && ctrl->weak.fetch_sub(1, std::memory_order_acq_rel) == 1) {
if (ctrl->strong.load(std::memory_order_acquire) == 0)
delete ctrl;
}
}
};
注意:
MySharedPtr内部没有直接提供构造函数接受控制块的版本。可以添加私有构造函数供lock()使用,或使用友元实现。
5. 常见陷阱与最佳实践
- 循环引用:
MySharedPtr相互持有导致计数永不归零。使用MyWeakPtr打破循环。 - 线程安全:计数器必须原子化;如果你还需要对
ptr做读写同步,需进一步加锁或使用std::shared_mutex。 - 自定义删除器:在构造
MySharedPtr时传入std::function<void(T*)>,支持数组删除、资源回收池等。 - 异常安全:所有操作在异常抛出前已确保计数正确更新。使用
std::atomic的memory_order_acq_rel能保证原子操作的完整性。 - 性能考虑:
std::shared_ptr的实现已高度优化。自定义实现可根据需求裁剪,例如不需要弱引用就省略相关字段。 - 内存泄漏:如果忘记删除控制块,可能导致内存泄漏。确保在计数为零时释放控制块。
6. 简单示例
int main() {
MySharedPtr <int> sp1(new int(42));
MyWeakPtr <int> wp(sp1);
std::cout << "use_count: " << sp1.use_count() << '\n'; // 1
if (auto sp2 = wp.lock()) {
std::cout << "locked value: " << *sp2 << '\n'; // 42
std::cout << "use_count after lock: " << sp2.use_count() << '\n'; // 2
}
sp1.~MySharedPtr(); // 手动析构
std::cout << "expired? " << std::boolalpha << wp.expired() << '\n'; // true
}
通过上述实现,你可以获得一个与标准库功能相似但可自由扩展的自定义智能指针。根据项目需求,你可以进一步添加:
- 对齐/内存池支持
- 对象生命周期回调
- 兼容
std::allocator的内存管理
以上即为在C++中实现自定义智能指针弱引用功能的完整思路与关键代码。祝你编码愉快!