如何证明C++11中make_shared的shared_ptr控制块需等weak_ptr计数归零才销毁?
核心问题
是否存在shared_ptr引用计数为0,但weak_ptr引用计数不为0的情况?
理论依据
当shared_ptr由make_shared创建且存在指向它的weak_ptr时,其控制块会一直存活,直到shared_ptr和weak_ptr的引用计数均归0。原文说明:
There must be a way for weak_ptrs to determine if the managed object is still valid (eg. for lock). They do this by checking the number of shared_ptrs that own the managed object, which is stored in the control block. The result is that the control blocks are alive until the shared_ptr count and the weak_ptr count both hit 0.
原测试代码的局限性
我曾尝试通过以下代码验证该结论,但两个use_count()均输出0,无法判断控制块是否存活:
#include<memory> #include<iostream> using namespace std; struct My { int m_i; My(int i) : m_i(i) { cout << "My ctor:" << m_i << '\n';} ~My() { cout << "My dtor:" << m_i << '\n';} // 修正原代码的笔误:析构函数输出应为dtor }; weak_ptr<My> wp1, wp2; int main() { { auto sp1 = shared_ptr<My>(new My(30)); wp1 = weak_ptr<My>(sp1); } cout<< wp1.use_count() << endl; { auto sp2 = make_shared<My>(40); wp2 = weak_ptr<My>(sp2); } cout<< wp2.use_count() << endl; return 0; }
注:use_count()返回的是shared_ptr的共享计数,无法直接反映weak_ptr的弱计数或控制块的存活状态。
验证控制块存活的有效方法
方法一:自定义分配器跟踪内存生命周期
通过自定义分配器,可以直观看到make_shared的内存分配特性——它会一次性分配托管对象+控制块的内存,只有当weak_ptr也销毁时,这块内存才会被释放。
#include <memory> #include <iostream> #include <new> // 自定义分配器,跟踪内存的分配与释放 template<typename T> struct TrackAllocator { using value_type = T; TrackAllocator() = default; template<typename U> TrackAllocator(const TrackAllocator<U>&) noexcept {} T* allocate(std::size_t n) { T* ptr = static_cast<T*>(std::malloc(n * sizeof(T))); std::cout << "分配内存:" << n * sizeof(T) << "字节 @ " << ptr << '\n'; return ptr; } void deallocate(T* ptr, std::size_t n) noexcept { std::cout << "释放内存:" << n * sizeof(T) << "字节 @ " << ptr << '\n'; std::free(ptr); } }; template<typename T, typename U> bool operator==(const TrackAllocator<T>&, const TrackAllocator<U>&) noexcept { return true; } template<typename T, typename U> bool operator!=(const TrackAllocator<T>&, const TrackAllocator<U>&) noexcept { return false; } struct My { int m_i; My(int i) : m_i(i) { std::cout << "My构造:" << m_i << '\n'; } ~My() { std::cout << "My析构:" << m_i << '\n'; } }; int main() { std::cout << "=== 用new创建shared_ptr ===" << '\n'; { std::weak_ptr<My> wp; { std::shared_ptr<My> sp(new My(30)); wp = sp; std::cout << "shared_ptr计数:" << sp.use_count() << '\n'; } std::cout << "weak_ptr是否过期:" << std::boolalpha << wp.expired() << '\n'; // 此时对象和控制块已分别释放 } std::cout << "\n=== 用make_shared创建shared_ptr ===" << '\n'; { std::weak_ptr<My> wp; { auto sp = std::allocate_shared<My>(TrackAllocator<My>(), 40); wp = sp; std::cout << "shared_ptr计数:" << sp.use_count() << '\n'; } std::cout << "weak_ptr是否过期:" << std::boolalpha << wp.expired() << '\n'; // 此时对象已析构,但包含控制块的内存还未释放 } std::cout << "\n=== 离开weak_ptr作用域 ===" << '\n'; // 此时weak_ptr销毁,控制块所在内存才会被释放 return 0; }
运行结果解析:
new创建的shared_ptr:shared_ptr销毁后,立即触发对象析构和控制块内存释放。make_shared创建的shared_ptr:shared_ptr销毁后仅触发对象析构,直到weak_ptr销毁时,才会释放包含控制块的整块内存。
方法二:模拟控制块的双计数逻辑
shared_ptr的控制块本质包含两个独立计数:
- 共享计数:记录
shared_ptr的数量,归0时销毁托管对象。 - 弱计数:记录
weak_ptr的数量,归0时销毁控制块。
以下是简化的模拟代码:
#include <iostream> #include <atomic> struct ControlBlock { std::atomic<int> shared_count = 1; std::atomic<int> weak_count = 0; // 当shared_ptr销毁时调用 void decrement_shared() { if (--shared_count == 0) { std::cout << "销毁托管对象\n"; // 若弱计数也为0,销毁控制块 if (weak_count == 0) { std::cout << "销毁控制块\n"; delete this; } } } // 当weak_ptr销毁时调用 void decrement_weak() { if (--weak_count == 0) { // 若共享计数已为0,销毁控制块 if (shared_count == 0) { std::cout << "销毁控制块\n"; delete this; } } } };
当shared_ptr销毁后共享计数归0,只要还有weak_ptr存在(弱计数>0),控制块就会继续存活;只有当所有weak_ptr也销毁,弱计数归0时,控制块才会被销毁。
结论
确实存在shared_ptr引用计数(共享计数)为0,但weak_ptr引用计数(弱计数)不为0的情况:
- 若
shared_ptr由make_shared创建,且存在关联的weak_ptr,shared_ptr销毁后共享计数归0,托管对象被销毁,但控制块因弱计数大于0仍存活,直到所有weak_ptr也被销毁。 - 若
shared_ptr通过new创建,控制块和对象内存分开分配,shared_ptr销毁后对象和控制块会立即被销毁(此时weak_ptr变为无效状态)。
内容的提问来源于stack exchange,提问作者Troskyvs

