shared_ptr是C++智能指针中使用频率最高的一个,但不少人对它的销毁机制只有模糊的印象:知道它靠引用计数管理生命周期,却说不清计数到底在什么时候加、什么时候减,也不清楚控制块和托管对象是不是一起销毁的。这篇文章围绕销毁顺序和引用计数变化两个核心问题展开,把析构链路拆开讲透。

一、先搞清楚shared_ptr的内部结构
一个shared_ptr实际上持有两个指针:一个指向托管对象,另一个指向控制块。控制块是shared_ptr库在堆上分配的一块独立内存,里面至少包含三个字段:强引用计数(use count)、弱引用计数(weak count)以及一个删除器(deleter)。强引用计数记录当前有多少个shared_ptr共享这个对象,弱引用计数记录有多少个weak_ptr在观察这个控制块。
这个双指针设计解释了一个常见疑惑:多个shared_ptr副本共享的是同一个控制块,但指针本身各自独立存在。拷贝一个shared_ptr时,新对象的成员指针简单复制,但控制块里的强引用计数会原子地加一。理解了这一点,后面的销毁顺序就很好推导了。
可以用一段简单的代码观察引用计数的变化:
#include <memory>
#include <iostream>
struct Demo {
~Demo() { std::cout << "Demo destroyed\n"; }
};
int main() {
std::shared_ptr<Demo> a = std::make_shared<Demo>();
std::cout << a.use_count() << "\n"; // 输出 1
{
std::shared_ptr<Demo> b = a; // 强引用计数 +1
std::cout << a.use_count() << "\n"; // 输出 2
} // b析构,强引用计数 -1,回到 1
std::cout << a.use_count() << "\n"; // 输出 1
} // a析构,强引用计数归 0,Demo 被销毁二、销毁顺序:对象和控制块不是一起死的
这是最容易被误解的一点。当最后一个shared_ptr被析构、强引用计数减到0时,销毁分两步走:第一步立即调用删除器销毁托管对象(通常是执行析构函数并释放对象内存);第二步等所有weak_ptr也过期、弱引用计数归0后,才释放控制块本身。也就是说,对象和控制块的销毁时机是解耦的。
这个设计是有意为之的。weak_ptr要能检测对象是否还活着,就必须依赖控制块的存在,所以控制块必须比对象活得更久。来看一个验证性的例子:
#include <memory>
#include <iostream>
struct Res {
~Res() { std::cout << "Res析构\n"; }
};
int main() {
std::weak_ptr<Res> wp;
{
auto sp = std::make_shared<Res>();
wp = sp;
} // sp离开作用域,强引用归0,Res析构
if (wp.expired()) {
std::cout << "对象已销毁,但控制块还活着\n";
}
// main结束时wp析构,弱引用归0,控制块内存被释放
}另外还有一个细节:如果对象是连同控制块一起通过make_shared分配的,标准库通常会把两块内存合并成一次分配,此时对象内存要到控制块释放时才一并归还,这是用少量内存驻留换取性能的典型权衡。
三、析构链与reset、赋值的计数变化
shared_ptr的析构函数逻辑可以概括为四步:如果持有控制块,先原子地把强引用计数减一;如果减完仍是正值,直接返回;如果是0,调用删除器销毁托管对象;再把弱引用计数减一,减到0则销毁控制块。赋值操作符和reset同样遵循这套逻辑,只是顺序上先处理新资源、再释放旧资源。
比如a = b这个赋值:先把b的控制块强引用计数加一(保证并发安全),再对a原本指向的控制块执行上述析构流程。这解释了为什么a = a自赋值是安全的——先加后减,中间计数不会跌到0。而reset()等价于赋一个空指针,会让当前shared_ptr放弃所有权,如果它是最后一个持有者,对象就地析构。
再看对象内部成员的析构顺序。当托管对象的析构函数被调用时,其成员按声明的相反顺序析构,这是C++语言层面的规则,与shared_ptr无关。但如果成员本身也是shared_ptr,就会触发嵌套的计数递减链,形成一条逐层向下的析构级联,示例如下:
#include <memory>
#include <iostream>
struct Inner {
~Inner() { std::cout << "Inner析构\n"; }
};
struct Outer {
std::shared_ptr<Inner> inner;
~Outer() { std::cout << "Outer析构\n"; }
};
int main() {
auto outer = std::make_shared<Outer>();
outer->inner = std::make_shared<Inner>();
} // 输出顺序:Outer析构 -> Inner析构 -> 控制块释放输出顺序是先打印Outer析构,再打印Inner析构。因为Outer的析构函数体执行完后,成员inner才会析构,inner的强引用计数归0进而触发Inner的销毁。这条链路在调试析构顺序问题时非常有用。
四、几个和销毁顺序相关的易错点
第一是循环引用。两个对象各持有对方的shared_ptr时,强引用计数永远无法归0,导致内存泄漏。解决办法是把其中一方改为weak_ptr,打破环状依赖,这也是weak_ptr最主要的实战用途。
第二是多线程场景。强引用计数的增减是原子操作,所以多个线程同时拷贝、析构不同的shared_ptr副本是安全的;但同一个shared_ptr对象被多线程读写则需要加锁。原子操作虽然保证正确性,却也带来一定性能开销,在高频传递时可以考虑传const引用减少计数波动。
第三是enable_shared_from_this。在对象内部获取指向自身的shared_ptr必须通过shared_from_this(),它依赖一个内部的weak_ptr,这也是弱引用计数存在的原因之一。如果直接用裸指针再包一层shared_ptr,会创建第二个控制块,同一个对象被计数两次,销毁时会触发双重析构,属于严重错误。
总结一下:shared_ptr的销毁遵循强引用归0销毁对象、弱引用归0销毁控制块的两阶段顺序;拷贝加计数、析构减计数;对象成员析构与声明顺序相反并可能级联触发嵌套析构。把这些规则记牢,排查智能指针相关的生命周期问题就会轻松很多。
C++ shared_ptr销毁顺序引用计数修改时间:2026-09-15 05:36:42