0

0

C++智能指针引用计数原理解析

P粉602998670

P粉602998670

发布时间:2025-09-22 14:22:01

|

981人浏览过

|

来源于php中文网

原创

引用计数通过共享所有权自动管理对象生命周期,解决内存泄漏与野指针问题;其核心是控制块中的强弱引用计数,配合原子操作确保线程安全;但需警惕循环引用、性能开销等挑战,可通过weak_ptr打破循环、优先使用make_shared优化分配,并根据所有权语义合理选择智能指针类型。

c++智能指针引用计数原理解析

C++智能指针中的引用计数,说白了,就是一种巧妙的内存管理机制,它让多个智能指针实例能够共同“拥有”同一个对象。当这个对象的所有者(也就是所有指向它的智能指针)都消失了,引用计数归零,对象也就自动被销毁了。它解决了手动管理内存时最头疼的内存泄漏和野指针问题,让程序员能更专注于业务逻辑,而不是整天提心吊胆地想着

delete

引用计数的核心在于为每一个被管理的对象维护一个计数器。每当有一个新的

std::shared_ptr
实例指向这个对象时,计数器就加一;每当一个
std::shared_ptr
实例不再指向这个对象(比如它被销毁了,或者被赋值了新的对象),计数器就减一。一旦计数器归零,这就意味着没有任何
std::shared_ptr
再关心这个对象了,此时,它就会被安全地销毁。这个计数器本身通常是原子操作的,以确保在多线程环境下也能正确地管理对象的生命周期。可以说,它就像一个隐形的管家,默默地为我们打理着对象的生老病死。

为什么需要引用计数,它解决了什么痛点?

在我看来,引用计数之所以成为现代C++不可或缺的一部分,因为它直接击中了C++传统内存管理的几个核心痛点。

最明显的,就是内存泄漏。想想看,我们用

new
分配了一块内存,然后可能因为程序逻辑复杂,或者异常发生,或者干脆就是粗心大意,忘了调用
delete
。结果呢?那块内存就成了“孤儿”,永远不会被回收,直到程序结束。如果这种情况频繁发生,系统资源就会被耗尽。

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

另一个让人头大的问题是野指针和重复释放。当多个原始指针指向同一个对象时,如果其中一个指针提前

delete
了对象,其他指针就变成了野指针,再去访问就会导致未定义行为甚至程序崩溃。更糟的是,如果多个指针都尝试
delete
同一个对象,那就会导致重复释放,这在操作系统层面是严重错误。

引用计数,特别是

std::shared_ptr
,就是为了解决这些问题而生的。它提供了一种共享所有权的语义。你不需要去判断什么时候该
delete
,也不用担心多个地方引用同一个对象时谁来负责销毁。只要还有
shared_ptr
指向它,对象就安然无恙;当最后一个
shared_ptr
消失,对象就自动、安全地被清理了。这就像是给对象设置了一个“生命维持系统”,只要还有“生命线”连接着,它就活着。这种自动化管理极大地提升了代码的健壮性和开发效率,让我可以把更多精力放在业务逻辑上,而不是与内存错误搏斗。

引用计数是如何在底层实现的?(以
std::shared_ptr
为例)

要理解

std::shared_ptr
的引用计数,就不得不提它的“幕后英雄”——控制块(Control Block)。这玩意儿通常是一个单独在堆上分配的小结构,它不直接存储你的对象,而是存储着关于你对象的一些管理信息。

一个典型的控制块至少会包含以下几个关键部分:

  1. 强引用计数(Strong Count):这就是我们常说的引用计数,一个整数(通常是
    std::atomic_long
    ,为了线程安全)。它记录了有多少个
    std::shared_ptr
    实例正在“拥有”这个对象。当它归零时,意味着对象可以被销毁了。
  2. 弱引用计数(Weak Count):这是为
    std::weak_ptr
    准备的。
    weak_ptr
    不拥有对象,所以它不会增加强引用计数,但它会增加弱引用计数。当强引用计数和弱引用计数都归零时,控制块本身才会被销毁。
  3. 指向被管理对象的指针:这是控制块真正指向你的
    T*
    对象的地方。
  4. 自定义删除器(Custom Deleter):如果你在创建
    shared_ptr
    时指定了特殊的删除逻辑(比如不是简单地
    delete
    ,而是
    fclose
    一个文件句柄),这个删除器就会存储在这里。
  5. 自定义分配器(Custom Allocator):如果你使用了自定义的内存分配器,相关信息也会在这里。

当一个

std::shared_ptr
被创建时(例如通过
std::make_shared
或从原始指针构造),如果它管理的内存还没有对应的控制块,就会先创建一个控制块。这个控制块的强引用计数被初始化为1,弱引用计数为0。

Live PPT
Live PPT

一款AI智能化生成演示内容的在线工具。只需输入一句话、粘贴一段内容、或者导入文件,AI生成高质量PPT。

下载
  • 复制构造或赋值一个
    std::shared_ptr
    时,只是简单地将源
    shared_ptr
    的控制块指针复制过来,然后将控制块里的强引用计数加一。
  • std::shared_ptr
    的析构函数
    被调用时,它会先将控制块里的强引用计数减一。
    • 如果强引用计数减到零,那么它就会调用之前存储的删除器(默认是
      delete
      )来销毁被管理的对象。
    • 接着,它会检查弱引用计数。如果此时强引用计数和弱引用计数都为零,那么控制块本身也会被销毁,释放掉它占用的内存。

这种分离的设计非常巧妙,它确保了即使所有

shared_ptr
都消失了,只要还有
weak_ptr
存在,控制块就不会立即销毁,
weak_ptr
仍然可以判断对象是否存活。这种机制在多线程环境下尤其重要,因为原子操作保证了计数的正确性,避免了竞态条件。

引用计数可能带来哪些问题和挑战,又该如何规避?

引用计数虽然强大,但它也不是银弹,在使用中也可能遇到一些问题和挑战,我们得学会如何规避它们。

首先,最经典也最令人头疼的就是循环引用(Circular References)。这是

shared_ptr
最著名的陷阱。当两个或多个对象通过
shared_ptr
相互持有对方的引用时,它们的强引用计数永远不会降到零,即使外部已经没有其他
shared_ptr
指向它们了,它们也无法被销毁,最终导致内存泄漏。比如,对象A有一个指向B的
shared_ptr
,同时对象B也有一个指向A的
shared_ptr
。它们互相依赖,谁也无法释放对方,形成了一个“死锁”般的循环。

规避方法:对于循环引用,解决方案通常是引入

std::weak_ptr
weak_ptr
是一种“非拥有”的智能指针,它不会增加对象的强引用计数。当你需要打破循环时,让其中一个对象持有另一个对象的
weak_ptr
而不是
shared_ptr
。这样,当外部对这两个对象的强引用都消失后,即使它们之间有
weak_ptr
的相互引用,强引用计数也能归零,对象就能被正常销毁了。在使用
weak_ptr
时,你需要通过
lock()
方法尝试获取一个
shared_ptr
,如果对象已经不存在了,
lock()
会返回一个空的
shared_ptr

其次,是性能开销

shared_ptr
的引用计数操作(增减)通常需要原子操作来保证多线程安全,这比普通的非原子操作要慢一些。此外,控制块通常需要单独的堆内存分配(除非使用
std::make_shared
),这也增加了额外的内存分配和访问开销。对于性能敏感的场景,这些开销是需要考虑的。

规避方法

  • 优先使用
    std::make_shared
    make_shared
    能够一次性分配对象和控制块所需的内存,减少了一次堆分配,并且通常能更好地利用缓存,提高性能。
  • 理解所有权语义:如果对象是独占所有权(只有一个地方拥有并负责销毁它),那么
    std::unique_ptr
    是更好的选择。
    unique_ptr
    几乎没有运行时开销,因为它不需要引用计数。只有在确实需要共享所有权时,才使用
    std::shared_ptr
  • 避免不必要的
    shared_ptr
    拷贝
    :每次拷贝都会导致原子操作。如果只是临时访问对象,可以考虑传递
    shared_ptr
    的引用,或者在确保对象存活的情况下,直接传递原始指针。
  • 性能分析:如果怀疑
    shared_ptr
    性能瓶颈,进行详细的性能分析是必要的,不要过早优化。

最后,引用计数也并非适用于所有资源管理场景。它主要针对的是堆内存对象的生命周期管理。对于文件句柄、网络连接、互斥锁等其他类型的资源,虽然

shared_ptr
可以配合自定义删除器来管理,但
std::unique_ptr
配合自定义删除器通常是更轻量和更合适的选择,因为它明确了资源的独占性。

规避方法:在选择智能指针时,始终先思考资源的所有权语义:是独占(

unique_ptr
),还是共享(
shared_ptr
),还是非拥有观察者(
weak_ptr
或原始指针)?根据实际需求选择最合适的智能指针,这能让你的代码更清晰、更高效、也更安全。理解它们的优缺点和适用场景,是写出高质量C++代码的关键。

相关专题

更多
counta和count的区别
counta和count的区别

Count函数用于计算指定范围内数字的个数,而CountA函数用于计算指定范围内非空单元格的个数。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

197

2023.11.20

fclose函数的用法
fclose函数的用法

fclose是一个C语言和C++中的标准库函数,用于关闭一个已经打开的文件,是文件操作中非常重要的一个函数,用于将文件流与底层文件系统分离,释放相关的资源。更多关于fclose函数的相关问题,详情请看本专题下面的文章。php中文网欢迎大家前来学习。

326

2023.11.30

堆和栈的区别
堆和栈的区别

堆和栈的区别:1、内存分配方式不同;2、大小不同;3、数据访问方式不同;4、数据的生命周期。本专题为大家提供堆和栈的区别的相关的文章、下载、课程内容,供大家免费下载体验。

389

2023.07.18

堆和栈区别
堆和栈区别

堆(Heap)和栈(Stack)是计算机中两种常见的内存分配机制。它们在内存管理的方式、分配方式以及使用场景上有很大的区别。本文将详细介绍堆和栈的特点、区别以及各自的使用场景。php中文网给大家带来了相关的教程以及文章欢迎大家前来学习阅读。

572

2023.08.10

线程和进程的区别
线程和进程的区别

线程和进程的区别:线程是进程的一部分,用于实现并发和并行操作,而线程共享进程的资源,通信更方便快捷,切换开销较小。本专题为大家提供线程和进程区别相关的各种文章、以及下载和课程。

481

2023.08.10

Python 多线程与异步编程实战
Python 多线程与异步编程实战

本专题系统讲解 Python 多线程与异步编程的核心概念与实战技巧,包括 threading 模块基础、线程同步机制、GIL 原理、asyncio 异步任务管理、协程与事件循环、任务调度与异常处理。通过实战示例,帮助学习者掌握 如何构建高性能、多任务并发的 Python 应用。

143

2025.12.24

数据库Delete用法
数据库Delete用法

数据库Delete用法:1、删除单条记录;2、删除多条记录;3、删除所有记录;4、删除特定条件的记录。更多关于数据库Delete的内容,大家可以访问下面的文章。

269

2023.11.13

drop和delete的区别
drop和delete的区别

drop和delete的区别:1、功能与用途;2、操作对象;3、可逆性;4、空间释放;5、执行速度与效率;6、与其他命令的交互;7、影响的持久性;8、语法和执行;9、触发器与约束;10、事务处理。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

209

2023.12.29

高德地图升级方法汇总
高德地图升级方法汇总

本专题整合了高德地图升级相关教程,阅读专题下面的文章了解更多详细内容。

27

2026.01.16

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
黑马云课堂mongodb实操视频教程
黑马云课堂mongodb实操视频教程

共11课时 | 3.1万人学习

DOM操作与实战案例
DOM操作与实战案例

共14课时 | 1.9万人学习

iOS应用UI控件开发基础视频
iOS应用UI控件开发基础视频

共148课时 | 31.3万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号