C++的std::shared_ptr是如何通过引用计数来管理内存的

std::shared_ptr通过引用计数机制自动管理内存,每个实例共享一个控制块,内含强弱引用计数、对象指针、自定义删除器等,确保对象在无拥有者时自动析构;循环引用需用std::weak_ptr打破;std::make_shared提升性能与异常安全。

c++的std::shared_ptr是如何通过引用计数来管理内存的

std::shared_ptr

通过一种叫做“引用计数”的机制来管理它所指向的动态分配对象的内存。简单来说,它会记录有多少个

shared_ptr

实例共同拥有同一个对象。每当一个新的

shared_ptr

指向这个对象时,计数就增加;每当一个

shared_ptr

实例被销毁或不再指向这个对象时,计数就减少。当这个引用计数归零时,就意味着没有

shared_ptr

再需要这个对象了,

shared_ptr

会自动地释放掉它所管理的内存。

解决方案

在我看来,

std::shared_ptr

是C++现代内存管理中最优雅的设计之一。它把复杂的资源生命周期管理简化成了一个几乎“隐形”的机制,让开发者能更专注于业务逻辑,而不是频繁地与

new

delete

打交道。

它的核心工作原理可以这样理解:每个

std::shared_ptr

实例,除了持有指向实际对象的指针外,还共同引用一个“控制块”(Control Block)。这个控制块才是引用计数的真正所在地。

当一个

shared_ptr

被创建或复制时(例如通过拷贝构造函数或赋值操作),它会与现有的

shared_ptr

共享同一个控制块,并且控制块中的“强引用计数”(strong reference count)会递增。这个强引用计数代表了有多少个

shared_ptr

实例正在“拥有”这个对象。

立即学习“C++免费学习笔记(深入)”;

反之,当一个

shared_ptr

实例被销毁(比如它离开了作用域),或者被重新赋值去管理另一个对象,又或者被

reset()

,那么它所关联的控制块中的强引用计数就会递减。

关键点来了:一旦这个强引用计数降到零,

std::shared_ptr

就知道这个对象不再被任何拥有者所需要了。此时,它会负责调用对象的析构函数,并释放对象所占用的内存。如果这个控制块还包含了“弱引用计数”(weak reference count,用于

std::weak_ptr

),并且弱引用计数也归零了,那么控制块本身的内存也会被释放。

这种机制有效地避免了手动内存管理中常见的“忘记

delete

”导致的内存泄漏,以及“重复

delete

”造成的程序崩溃。它把所有权的概念清晰化,让资源管理变得自动化且安全。

std::shared_ptr

的控制块里到底藏了些什么?

说到

shared_ptr

的魔法,我觉得它的控制块(Control Block)是真正值得深挖的地方。它可不仅仅是简单地存了个数字那么简单。这个小小的结构体,是

shared_ptr

能如此智能地管理内存的关键。

通常,一个控制块至少会包含以下几个核心元素:

强引用计数(Strong Reference Count):这就是我们一直在说的,有多少个

std::shared_ptr

实例正在“拥有”这个对象。每当一个

shared_ptr

被创建或复制,这个计数就加一;当

shared_ptr

被销毁或重置,这个计数就减一。当它归零时,

shared_ptr

会触发对象的析构和内存释放。这是对象生命周期管理的主宰。

弱引用计数(Weak Reference Count):这个是为

std::weak_ptr

准备的。

weak_ptr

不拥有对象,所以它不会增加强引用计数。但它会增加弱引用计数。即使强引用计数降到零,只要弱引用计数不为零,控制块本身就不会被销毁。这允许

weak_ptr

在对象被销毁后仍然能判断对象是否存活,并安全地返回空指针。

指向被管理对象的原始指针:控制块需要知道它到底管理的是哪个对象。这个指针就是指向我们用

new

make_shared

创建的那个实际对象。

自定义删除器(Custom Deleter):这是一个可选但非常强大的特性。有时,我们管理的资源不是通过

new

分配的,或者需要特殊的清理逻辑(比如文件句柄、数据库连接、C风格的

malloc

分配的内存)。

shared_ptr

允许你在创建时传入一个函数对象或lambda表达式作为删除器。当强引用计数归零时,

shared_ptr

会调用这个自定义删除器来释放资源,而不是简单地调用

delete

。这极大地扩展了

shared_ptr

的应用范围。

自定义分配器(Custom Allocator):同样是可选的,用于在创建对象或控制块时使用特定的内存分配策略。这在某些高性能或内存受限的场景下会很有用。

所以,控制块远不止一个计数器。它是一个包含了所有必要信息和回调的枢纽,确保了对象生命周期的正确管理,并提供了高度的灵活性。

为什么

std::shared_ptr

能解决循环引用问题,但又不能完全避免?

这是一个非常经典的问题,也是

shared_ptr

使用中一个常见的陷阱。我觉得理解这一点,是真正掌握智能指针的关键一步。

std::shared_ptr

本身确实无法解决循环引用问题。当两个对象通过

shared_ptr

相互持有对方时,它们会形成一个“闭环”。比如,对象A有一个

shared_ptr

指向B,对象B有一个

shared_ptr

指向A。这时,A的强引用计数至少是1(被B持有),B的强引用计数至少是1(被A持有)。即使外部所有指向A和B的

shared_ptr

都销毁了,A和B的强引用计数也永远不会降到零,因为它们彼此互相持有。结果就是,这两个对象永远不会被析构,内存也就泄漏了。

这就像两个人都握着对方的手,谁都不肯先松开,结果就是谁也走不了。

那么,我们怎么解决呢?答案是引入

std::weak_ptr

weak_ptr

被称为“弱引用”或“非拥有引用”,它扮演的角色是“观察者”而不是“拥有者”。

weak_ptr

的妙处在于:

它指向一个由

shared_ptr

管理的对象,但它不会增加对象的强引用计数。它允许你检查所指向的对象是否仍然存活。如果对象已经被销毁,

weak_ptr

会知道这一点。你可以尝试通过

weak_ptr::lock()

方法获取一个

shared_ptr

。如果对象还存活,

lock()

会返回一个有效的

shared_ptr

(并临时增加强引用计数);如果对象已被销毁,它会返回一个空的

shared_ptr

所以,解决循环引用的策略就是:在构成循环引用的两个对象中,让其中一个指针变成

std::weak_ptr

举个例子,假设我们有

Parent

Child

两个类,

Parent

包含

shared_ptr

Child

也需要一个指向

Parent

的引用。如果

Child

也用

shared_ptr

,那就会形成循环。正确的做法是:

Parent

拥有

Child

shared_ptr

),而

Child

观察

Parent

weak_ptr

)。这样,

Child

Parent

的引用不会增加

Parent

的强引用计数,从而打破了循环。当所有外部对

Parent

shared_ptr

都销毁后,

Parent

的强引用计数会归零,它会被正常析构,然后

Parent

的析构函数会销毁它所持有的

Child

对象。

所以,

std::shared_ptr

本身并不能自动避免循环引用,它只是提供了一个工具——

std::weak_ptr

——来让我们手动解决这个问题。这要求我们在设计类关系时,需要仔细思考对象之间的所有权关系,哪些是“拥有”,哪些只是“观察”。

std::make_shared

比直接

new

shared_ptr

构造有什么优势?

这其实是我在日常开发中,几乎总是推荐使用

std::make_shared

的原因。它不仅仅是代码看起来更简洁,背后还有实实在在的性能和安全性考量。

当你这样做时:

std::shared_ptr ptr(new MyClass());

这里发生了两次独立的内存分配:

new MyClass()

:为

MyClass

对象本身分配内存。

std::shared_ptr(...)

:为

shared_ptr

的控制块分配内存。

而当你使用

std::make_shared

时:

auto ptr = std::make_shared();

这里通常只发生一次内存分配。

std::make_shared

被设计成可以一次性分配足够的内存,来同时容纳

MyClass

对象和

shared_ptr

所需的控制块。

这种“一次分配”带来的优势是多方面的:

性能提升:减少了一次系统调用(内存分配通常是开销较大的操作),这在大量创建

shared_ptr

的场景下,可以带来显著的性能提升。更好的缓存局部性:对象本身和它的控制块在内存中是紧邻的,这有助于CPU缓存的利用率,提升程序的整体性能。异常安全:这是一个非常关键的优势。考虑一个函数调用:

some_function(std::shared_ptr(new MyClass()), another_function());

在C++标准中,函数参数的求值顺序是不确定的。编译器可能按以下顺序执行:a.

new MyClass()

b.

another_function()

c.

std::shared_ptr(...)

如果

new MyClass()

成功了,但在

std::shared_ptr

构造完成之前,

another_function()

抛出了一个异常,那么

new

出来的

MyClass

对象将永远不会被

shared_ptr

接管,从而导致内存泄漏。而使用

std::make_shared

some_function(std::make_shared(), another_function());
std::make_shared

会原子性地完成对象和控制块的分配与构造。如果它成功了,那么

shared_ptr

就完全拥有了对象。如果

another_function()

抛出异常,

make_shared

创建的对象也能被正确清理,避免了泄漏。

所以,除非你有非常特殊的需求(比如需要自定义删除器来处理非

new

分配的内存,或者需要一个特殊的分配器只用于对象本身而不用在控制块上),我个人总是倾向于使用

std::make_shared

。它是一个简单但强大的优化和安全实践。

以上就是C++的std::shared_ptr是如何通过引用计数来管理内存的的详细内容,更多请关注创想鸟其它相关文章!

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 chuangxiangniao@163.com 举报,一经查实,本站将立刻删除。
发布者:程序猿,转转请注明出处:https://www.chuangxiangniao.com/p/1474944.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
C++对象生命周期管理与RAII模式结合
上一篇 2025年12月18日 22:00:20
如何使用工具(如Valgrind)来检测C++程序的内存泄漏
下一篇 2025年12月18日 22:00:36

相关推荐

  • 如何在Dask中训练AI大模型?分布式数据处理的AI训练技巧

    如何在Dask中训练AI大模型?分布式数据处理的AI训练技巧如何在Dask中训练AI大模型?分布式数据处理的AI训练技巧如何在Dask中训练AI大模型?分布式数据处理的AI训练技巧如何在Dask中训练AI大模型?分布式数据处理的AI训练技巧

    Dask在处理超大规模数据集时的独特优势在于其Python原生的分布式计算能力,能无缝扩展Pandas和NumPy的工作流,突破单机内存限制,实现高效的数据预处理与模型训练。它通过惰性计算、分块处理和内存溢写机制,支持TB级数据的并行操作,相比Spark提供了更贴近Python数据科学生态的API和…

    2026年9月22日 用户投稿
    100
  • 抖音小店网页版怎么登录?抖音我的小店在哪里

    随着抖音电商平台的快速发展,越来越多的商家选择入驻该平台。作为商家运营的重要工具之一,抖音小店网页版为店铺管理带来了诸多便利。那么,如何正确登录抖音小店网页版?又该如何找到“我的小店”?下面将为您详细介绍。 一、为什么需要登录抖音小店网页版? 通过抖音小店网页版,商家可以高效地进行商品管理、订单处理…

    2026年9月22日
    000
  • 如何设置Linux用户磁盘配额 xfs_quota配置完整流程

    如何设置Linux用户磁盘配额 xfs_quota配置完整流程如何设置Linux用户磁盘配额 xfs_quota配置完整流程如何设置Linux用户磁盘配额 xfs_quota配置完整流程如何设置Linux用户磁盘配额 xfs_quota配置完整流程

    linux用户磁盘配额是通过xfs_quota工具配置,以限制用户或组的磁盘空间和文件数量。1. 确认文件系统为xfs并安装xfsprogs;2. 修改/etc/fstab启用usrquota和grpquota后重新挂载;3. 使用xfs_quota初始化数据库;4. 用limit命令设置用户或组的…

    2026年9月22日 用户投稿
    000
  • php-gd怎么应用复古滤镜_php-gd图像怀旧色调处理

    使用PHP-GD库实现复古滤镜主要通过色调偏移和色彩调整模拟老照片效果。1. 色调偏黄褐色:先转灰度,再用imagefilter添加棕黄色调;2. 手动像素级调整:逐像素计算灰度并赋予暖色系值,降低饱和度;3. 增强质感:结合对比度降低与轻微模糊提升真实感;4. 示例流程包括加载图像、应用滤镜、输出…

    2026年9月22日
    100
  • win11家庭版怎么升级到专业版_win11家庭版升级到专业版操作方法

    可通过系统设置输入专业版密钥升级,2. 或使用Media Creation Tool就地升级保留文件,3. 企业用户还可通过命令提示符部署KMS密钥激活,三种方法均能将Windows 11家庭版升级为专业版。 如果您希望在保留现有文件和设置的情况下,将功能较为基础的Windows 11家庭版升级为支…

    2026年9月22日
    1400
  • VSCode调试FPGA的UART通信(串口数据分析,调试技巧)

    使用VSCode调试FPGA的UART通信,核心是通过其扩展生态集成串口监视与数据分析。首先确保FPGA的UART模块正常工作并输出调试信息,然后在VSCode中安装“Serial Monitor”等串口扩展,配置波特率、端口号以捕获数据。为解析十六进制或自定义协议数据,可结合Python脚本通过t…

    2026年9月22日
    000
  • 如何扫描Linux本地网络 nmap基础扫描技巧

    如何扫描Linux本地网络 nmap基础扫描技巧如何扫描Linux本地网络 nmap基础扫描技巧如何扫描Linux本地网络 nmap基础扫描技巧如何扫描Linux本地网络 nmap基础扫描技巧

    快速扫描整个子网可使用 sudo nmap -sn 192.168.1.0/24,用于发现活跃主机;若防火墙屏蔽icmp请求,可加 -pe 参数提高准确性。2. 扫描单台设备开放端口用 sudo nmap 192.168.1.100,默认扫描1000个常见端口,或加 -p- 扫描全部端口,并可用 -…

    2026年9月22日 用户投稿
    100
  • 如何在mysql中监控用户操作日志

    MySQL默认不记录用户操作日志,但可通过启用通用查询日志记录所有SQL操作,或使用二进制日志追踪数据变更,也可部署审计插件实现细粒度监控,结合独立账号管理和日志轮转策略提升安全性与可追溯性。 MySQL 本身不默认记录用户的所有操作日志,但可以通过启用特定的日志功能来实现对用户行为的监控。以下是几…

    2026年9月22日
    000
  • 苹果手机为什么无法连接蓝牙耳机

    先检查蓝牙开关、耳机电量及配对模式,确认设备靠近且无干扰,重启设备后删除旧连接记录并重连,更新系统或重置网络设置,若均无效则可能存在硬件故障需联系官方支持。 苹果手机连不上蓝牙耳机,通常不是大问题,多是设置或环境的小疏忽。先别着急,按下面几步检查,基本能解决大部分情况。 基础状态确认 连接失败往往出…

    2026年9月22日
    000
  • 爱应用pc版官网访问地址 爱应用pc版平台官方链接直达首页

    爱应用PC版官网访问地址是http://www.xapcn.com/,该软件为WP7/WP8手机提供资源管理、软件游戏免费安装等服务。 爱应用pc版官网访问地址在哪里?这是不少网友都关注的,接下来由PHP小编为大家带来爱应用pc版平台官方链接直达首页,感兴趣的网友一起随小编来瞧瞧吧! http://…

    2026年9月22日
    100
  • windows10蓝牙已配对但未连接怎么办_windows10蓝牙配对未连接解决方法

    windows10蓝牙已配对但未连接怎么办_windows10蓝牙配对未连接解决方法windows10蓝牙已配对但未连接怎么办_windows10蓝牙配对未连接解决方法windows10蓝牙已配对但未连接怎么办_windows10蓝牙配对未连接解决方法windows10蓝牙已配对但未连接怎么办_windows10蓝牙配对未连接解决方法

    1、重启蓝牙支持服务并设为自动启动;2、更新或重装蓝牙驱动程序;3、删除设备后重新配对;4、运行Windows蓝牙疑难解答;5、检查设备电量与可发现模式,确保其正常工作。 如果您已成功将蓝牙设备与计算机配对,但设备状态显示为“已配对”却无法连接并使用,则可能是由于驱动程序、服务设置或系统缓存问题导致…

    2026年9月22日 用户投稿
    000
  • 宇宙级编辑器VSCode你真的会用吗?这些隐藏功能让效率翻倍​​

    VSCode的真正潜力在于深度使用命令面板、多光标编辑、用户代码片段、集成终端与任务、自定义快捷键及扩展生态,通过主动探索设置、状态栏功能、官方文档与社区资源,结合个性化主题与高效扩展,将其从基础编辑器升级为高度定制化、自动化、无缝集成的专属开发利器,显著提升编码效率与体验。 你可能以为自己会用VS…

    2026年9月22日
    000
  • mysql安装完如何连接 mysql安装后的客户端使用教程

    mysql安装完如何连接 mysql安装后的客户端使用教程mysql安装完如何连接 mysql安装后的客户端使用教程mysql安装完如何连接 mysql安装后的客户端使用教程mysql安装完如何连接 mysql安装后的客户端使用教程

    连接mysql的方法包括命令行连接本地数据库、配置远程访问权限、使用图形化工具及排查连接问题。1. 使用命令行输入mysql -u root -p并输入密码登录,若未设密码可省略-p;2. 创建远程用户并授权:create user ‘newuser’@’%&#8…

    2026年9月22日 用户投稿
    200
  • Qoder上线提示词增强功能 将开发者从“提示词”的负担中解放出来

    在 agentic coding 的新时代,一个关键挑战日益凸显:要得到卓越的答案,你必须先提出卓越的问题。 对开发者而言,这意味着需要投入大量时间去精心设计给ai的“提示词”。一句笼统的指令,比如“帮我写个函数”,往往只能换来一段简陋甚至存在安全隐患的代码;而一条清晰、结构完整、细节丰富的提示,则…

    2026年9月22日
    000
  • 如何用RunwayML导出AI生成的图片?高效保存图像的实用教程

    导出RunwayML生成的图片需先完成生成任务并进入详情视图,点击“下载”选择PNG或JPG等格式,推荐PNG以保留高质量细节;批量导出时使用多选功能统一设置分辨率和格式,提升效率;建议采用项目化文件夹结构与规范化命名规则管理海量图片,并利用标签、云同步辅助整理;后续应用中可结合Photoshop、…

    2026年9月22日
    100
  • 抖音账号定位技巧:普通人如何选择最适合自己的内容方向?

    抖音账号定位技巧:普通人如何选择最适合自己的内容方向?抖音账号定位技巧:普通人如何选择最适合自己的内容方向?抖音账号定位技巧:普通人如何选择最适合自己的内容方向?抖音账号定位技巧:普通人如何选择最适合自己的内容方向?

    做抖音最容易犯的错误是内容随意,导致涨粉慢、互动低,明确目标和定位才能精准吸引用户。一、先搞清楚做抖音的目的,带货卖货选实用性强的领域,打造个人品牌需突出专业性,兴趣分享也要有核心主题;二、从“你能做的”“你感兴趣的”“用户有需求的”找交集,确定内容方向;三、参考对标账号的内容形式、标题风格、拍摄手…

    2026年9月22日 用户投稿
    200
  • 为什么不应该在多个网站重复使用同一个密码,撞库攻击的原理是什么?

    撞库攻击利用用户重复使用密码的习惯,黑客通过泄露的账号密码批量尝试登录其他网站,一旦某个小网站密码泄露,相同密码的高价值账户也可能被攻破,因此每个网站应使用独立复杂密码并开启双重验证以提升安全。 不建议在多个网站用同一个密码,核心原因就是防范“撞库攻击”。这种攻击利用了用户重复使用密码的习惯,让黑客…

    2026年9月22日
    700
  • 定制Spring Boot Kafka自动配置:构建可复用的配置注解

    本文深入探讨了在Spring Boot应用中,如何通过自定义注解实现Kafka配置的自动化与简化。面对传统@PostConstruct方法注册KafkaTemplate导致Bean无法注入的问题,文章详细介绍了两种更健壮的解决方案:利用META-INF/spring.factories实现真正的自动…

    2026年9月22日
    100
  • VSCode极简配置Python:中文界面、代码补全、虚拟环境

    安装中文语言包实现界面汉化;2. 通过Microsoft官方Python扩展启用Pylance获得智能补全;3. 使用VSCode内置功能创建并管理项目级虚拟环境;4. 推荐Black、isort、GitLens等插件提升开发效率。 用VSCode配置Python开发环境,想要做到中文界面、流畅的代…

    2026年9月22日
    300
  • Laravel控制器怎么创建_Laravel控制器创建与请求处理

    Laravel控制器处理请求,使用Artisan命令php artisan make:controller创建,带–resource参数可生成CRUD方法;通过引入Request类获取输入并验证数据,在路由文件中绑定URL与控制器方法,实现请求响应流程。 在 Laravel 中,控制器是…

    2026年9月22日
    600

发表回复

登录后才能评论
关注微信