导读:本期聚焦于广州程序员创作的《C++ shared_ptr销毁顺序是怎样的?引用计数变化过程详解》,敬请观看详情。为什么同一个对象的多个shared_ptr副本销毁时不会立刻释放资源?为什么成员的析构顺序和声明顺序相反?这些问题背后都藏着引用计数的运作机制。本文从shared_ptr的控制块结构讲起,逐步分析拷贝、赋值、reset、作用域结束等场景下引用计数的增减规律,说明强引用与弱引用计数的区别,解释对象与控制块的销毁时机为何不同,并梳理析构链的完整触发过程。文末还汇总了循环引用、多线程原子开销、enable_shared_from_this等常见易错点,帮你彻底搞懂shared_ptr的生命周期管理。

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

C++ shared_ptr销毁顺序是怎样的?引用计数变化过程详解

一、先搞清楚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

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/20260915/57069.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。