在 C++ 标准库中,std::shared_ptr 和 std::weak_ptr 一起提供了对共享对象的自动内存管理。std::weak_ptr 通过弱引用避免了循环引用导致的内存泄漏,并在访问对象前可以检查对象是否已被销毁。本文将演示如何在 C++20 环境下手动实现一个最简洁的弱引用类 WeakPtr<T>,并说明其工作原理、使用场景以及如何与自定义引用计数结合。
1. 设计思路
核心目标是实现:
- 弱引用计数:记录还有多少
WeakPtr指向同一对象。 - 共享计数:记录有多少
SharedPtr(或我们自定义的SharedPtr)仍在持有对象。 - 自动销毁:当共享计数为 0 时,销毁对象;当弱计数为 0 时,释放内部结构。
实现方式:
- 使用一个独立的 控制块(Control Block) 存放计数和对象指针。
- `SharedPtr `(示例实现)持有指向控制块的指针并递增共享计数。
- `WeakPtr ` 同样持有指向控制块的指针,但递增弱计数。
- `WeakPtr ::lock()` 能在共享计数>0 时返回对应的 `SharedPtr`,否则返回 `nullptr`。
2. 代码实现
以下代码使用 C++20 特性(如 std::unique_ptr、std::addressof)实现了一个极简的弱指针。为保持重点清晰,示例中省略了异常安全和线程安全等细节。
#pragma once
#include <cstddef>
#include <memory>
#include <utility>
#include <atomic>
#include <iostream>
template<typename T>
struct ControlBlock
{
std::atomic<std::size_t> strong{0}; // SharedPtr计数
std::atomic<std::size_t> weak{0}; // WeakPtr计数
T* ptr; // 被管理的对象
explicit ControlBlock(T* p) : ptr(p) {}
};
template<typename T>
class SharedPtr;
template<typename T>
class WeakPtr
{
public:
WeakPtr() noexcept : cb(nullptr) {}
WeakPtr(const WeakPtr& other) noexcept : cb(other.cb)
{
if (cb) ++cb->weak;
}
WeakPtr(WeakPtr&& other) noexcept : cb(std::exchange(other.cb, nullptr)) {}
explicit WeakPtr(SharedPtr <T> const& sp);
~WeakPtr()
{
if (cb && !--cb->weak && cb->strong == 0) {
delete cb;
}
}
WeakPtr& operator=(const WeakPtr& rhs) noexcept
{
if (this != &rhs) {
WeakPtr tmp(rhs);
std::swap(cb, tmp.cb);
}
return *this;
}
WeakPtr& operator=(WeakPtr&& rhs) noexcept
{
if (this != &rhs) {
WeakPtr tmp(std::move(rhs));
std::swap(cb, tmp.cb);
}
return *this;
}
SharedPtr <T> lock() const noexcept
{
if (cb && cb->strong > 0) {
return SharedPtr <T>(*this);
}
return SharedPtr <T>();
}
bool expired() const noexcept
{
return !cb || cb->strong == 0;
}
private:
ControlBlock <T>* cb;
friend class SharedPtr <T>;
};
template<typename T>
class SharedPtr
{
public:
SharedPtr() noexcept : cb(nullptr), ptr(nullptr) {}
explicit SharedPtr(T* p) : cb(nullptr), ptr(p)
{
if (p) {
cb = new ControlBlock <T>(p);
cb->strong = 1;
}
}
SharedPtr(const SharedPtr& other) noexcept : cb(other.cb), ptr(other.ptr)
{
if (cb) ++cb->strong;
}
SharedPtr(SharedPtr&& other) noexcept
: cb(std::exchange(other.cb, nullptr)), ptr(std::exchange(other.ptr, nullptr)) {}
explicit SharedPtr(WeakPtr <T> const& wp) noexcept : cb(wp.cb), ptr(wp.cb ? wp.cb->ptr : nullptr)
{
if (cb && cb->strong > 0) {
++cb->strong;
} else {
cb = nullptr;
ptr = nullptr;
}
}
~SharedPtr()
{
if (cb && !--cb->strong) {
delete ptr;
if (cb->weak == 0) delete cb;
}
}
SharedPtr& operator=(const SharedPtr& rhs) noexcept
{
SharedPtr tmp(rhs);
std::swap(cb, tmp.cb);
std::swap(ptr, tmp.ptr);
return *this;
}
SharedPtr& operator=(SharedPtr&& rhs) noexcept
{
SharedPtr tmp(std::move(rhs));
std::swap(cb, tmp.cb);
std::swap(ptr, tmp.ptr);
return *this;
}
T* operator->() const noexcept { return ptr; }
T& operator*() const noexcept { return *ptr; }
T* get() const noexcept { return ptr; }
std::size_t use_count() const noexcept { return cb ? cb->strong : 0; }
private:
ControlBlock <T>* cb;
T* ptr;
};
template<typename T>
WeakPtr <T>::WeakPtr(SharedPtr<T> const& sp) noexcept : cb(sp.cb)
{
if (cb) ++cb->weak;
}
关键点说明
- 控制块:存放共享计数、弱计数和指向对象的裸指针。使用
std::atomic保证多线程环境下计数的原子性。 SharedPtr:- 构造时若传入裸指针,创建新的控制块并把共享计数设为1。
- 拷贝构造/赋值时递增共享计数。
- 销毁时递减共享计数;若变为0,删除对象;若弱计数也为0,删除控制块。
WeakPtr:- 拷贝构造/赋值时递增弱计数。
- 销毁时递减弱计数;若共享计数已为0且弱计数变为0,删除控制块。
lock()用来尝试获取对应的SharedPtr,只有当共享计数>0时才成功。
3. 使用示例
#include <iostream>
struct Foo {
Foo() { std::cout << "Foo ctor\n"; }
~Foo() { std::cout << "Foo dtor\n"; }
void greet() const { std::cout << "Hello from Foo!\n"; }
};
int main()
{
SharedPtr <Foo> sp1(new Foo); // sp1 用来管理对象
{
WeakPtr <Foo> wp = sp1; // wp 形成弱引用
if (!wp.expired()) {
auto sp2 = wp.lock(); // 尝试获得共享指针
sp2->greet(); // 输出问候
}
} // wp 作用域结束,WeakPtr 被销毁
std::cout << "sp1 use_count: " << sp1.use_count() << '\n';
return 0;
}
运行结果示例:
Foo ctor
Hello from Foo!
sp1 use_count: 1
Foo dtor
4. 常见问题与最佳实践
-
线程安全:上述实现已使用
std::atomic计数,但未实现完整的线程安全,例如WeakPtr::lock()的检查-递增操作不是原子。实际项目中可使用std::shared_ptr的实现或加入互斥锁。 -
异常安全:构造控制块时若
new抛出异常,未持有任何计数。若在构造过程中出现异常,需确保已析构。 -
自定义删除器:标准
std::shared_ptr支持自定义 deleter。若需要同样功能,可在控制块中存储std::function<void(T*)>并在销毁时调用。 -
多继承与多层包装:若
T是多重继承或包装类型,注意正确管理裸指针和控制块生命周期。
5. 结语
通过以上代码,你可以在 C++20 中手动实现一个基本的弱引用机制。它演示了控制块、计数管理以及弱指针与共享指针之间的协作方式。实际开发中,建议直接使用标准库 std::shared_ptr 与 std::weak_ptr,因为它们已被充分测试、优化并具备完整的异常安全与线程安全保证。若你对内存管理细节有兴趣,或者想构建自定义的引用计数系统,这份实现是一个很好的起点。