0

0

C++shared_ptr共享资源管理方法解析

P粉602998670

P粉602998670

发布时间:2025-09-02 09:06:02

|

956人浏览过

|

来源于php中文网

原创

std::shared_ptr通过引用计数实现共享所有权,自动管理对象生命周期,避免内存泄漏和悬空指针;使用std::make_shared可提升性能与异常安全;需警惕循环引用,可用std::weak_ptr打破;其引用计数线程安全,但被管理对象的并发访问仍需额外同步机制。

c++shared_ptr共享资源管理方法解析

C++的

std::shared_ptr
,在我看来,是现代C++处理动态内存和资源共享时的一把利器,它通过引入引用计数机制,巧妙地解决了多个所有者共同管理同一块内存的复杂性,避免了传统裸指针可能导致的内存泄漏和悬空指针问题,让资源管理变得更加自动化和安全。它本质上就是一种智能指针,能够确保被它管理的对象在不再被任何
shared_ptr
引用时,能够被正确、及时地销毁。

解决方案

std::shared_ptr
的核心思想是“共享所有权”。当你创建一个
shared_ptr
来管理一个对象时,它会内部维护一个引用计数。每当这个
shared_ptr
被复制(无论是通过拷贝构造函数、拷贝赋值操作符,还是作为函数参数传递),引用计数就会增加。这意味着有更多的
shared_ptr
实例正在“关注”这个对象。反之,当一个
shared_ptr
实例被销毁(例如,超出作用域、被
reset()
,或者被赋值为另一个
shared_ptr
),引用计数就会减少。一旦引用计数归零,就意味着没有任何
shared_ptr
实例再关心这个对象了,此时,
shared_ptr
会自动调用对象的析构函数并释放其所占用的内存。

这种机制的强大之处在于,它将资源的生命周期管理从程序员手中解放出来,自动化地处理了许多原本容易出错的场景。比如,你可以在不同的数据结构中存储指向同一个对象的

shared_ptr
,而无需担心谁应该负责
delete
这个对象。只要有一个
shared_ptr
仍然存活,对象就会一直存在。

创建

shared_ptr
通常推荐使用
std::make_shared

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

#include 
#include 
#include 

class MyResource {
public:
    std::string name;
    MyResource(const std::string& n) : name(n) {
        std::cout << "MyResource " << name << " created." << std::endl;
    }
    ~MyResource() {
        std::cout << "MyResource " << name << " destroyed." << std::endl;
    }
};

void processResource(std::shared_ptr res) {
    std::cout << "Processing: " << res->name << ", current count: " << res.use_count() << std::endl;
} // res goes out of scope, ref count might decrease

int main() {
    // 推荐使用 std::make_shared
    std::shared_ptr res1 = std::make_shared("Data A");
    std::cout << "Initial count for Data A: " << res1.use_count() << std::endl;

    {
        std::shared_ptr res2 = res1; // 拷贝,引用计数增加
        std::cout << "Count after copy: " << res1.use_count() << std::endl;
        processResource(res2); // 传递拷贝,函数内部又增加一次,然后减少
        std::cout << "Count after function call: " << res1.use_count() << std::endl;
    } // res2 goes out of scope, ref count decreases

    std::cout << "Count before main scope ends: " << res1.use_count() << std::endl;
    // main 结束时,res1 销毁,引用计数归零,MyResource "Data A" 被销毁
    return 0;
}

这段代码清晰地展示了

shared_ptr
如何通过引用计数管理
MyResource
对象的生命周期。

std::shared_ptr
循环引用:一个隐蔽的内存泄漏陷阱?

没错,

shared_ptr
虽然强大,但它有一个著名的“阿喀琉斯之踵”——循环引用。这听起来有点抽象,但实际场景中并不少见。想象一下,如果对象A持有一个指向对象B的
shared_ptr
,同时对象B也持有一个指向对象A的
shared_ptr
,会发生什么?

A -> shared_ptr
B -> shared_ptr

在这种情况下,当A和B的外部所有

shared_ptr
都消失后,A的引用计数永远不会降到1(因为B还持有一个),B的引用计数也永远不会降到1(因为A还持有一个)。它们互相持有对方的“所有权”,导致引用计数永远无法归零,从而谁也无法被销毁。这就是一个典型的内存泄漏,而且是那种非常隐蔽、难以调试的泄漏。

解决这个问题的关键在于引入

std::weak_ptr
weak_ptr
是一种不拥有所有权的智能指针。它观察一个由
shared_ptr
管理的对象,但不会增加对象的引用计数。你可以把它看作是一个“旁观者”或者“观察者”。当
shared_ptr
管理的对象被销毁时,所有关联的
weak_ptr
都会自动失效。

要访问

weak_ptr
所指向的对象,你需要先将其转换为
shared_ptr
,通过调用
weak_ptr::lock()
方法。如果对象仍然存活,
lock()
会返回一个有效的
shared_ptr
;如果对象已经被销毁,
lock()
则返回一个空的
shared_ptr

#include 
#include 
#include 

class B; // 前向声明

class A {
public:
    std::shared_ptr b_ptr;
    std::string name;
    A(const std::string& n) : name(n) { std::cout << "A " << name << " created." << std::endl; }
    ~A() { std::cout << "A " << name << " destroyed." << std::endl; }
};

class B {
public:
    std::weak_ptr a_ptr; // 使用 weak_ptr 解决循环引用
    std::string name;
    B(const std::string& n) : name(n) { std::cout << "B " << name << " created." << std::endl; }
    ~B() { std::cout << "B " << name << " destroyed." << std::endl; }

    void print_a_name() {
        if (auto sharedA = a_ptr.lock()) { // 尝试获取 shared_ptr
            std::cout << "B " << name << " accesses A: " << sharedA->name << std::endl;
        } else {
            std::cout << "A is no longer available for B " << name << std::endl;
        }
    }
};

int main() {
    std::shared_ptr myA = std::make_shared("Instance A");
    std::shared_ptr myB = std::make_shared("Instance B");

    // 建立连接
    myA->b_ptr = myB;
    myB->a_ptr = myA; // 这里是 weak_ptr,不会增加 A 的引用计数

    std::cout << "A's ref count: " << myA.use_count() << std::endl; // 应该是 1 (myA)
    std::cout << "B's ref count: " << myB.use_count() << std::endl; // 应该是 1 (myB)

    myB->print_a_name(); // B 可以安全地访问 A

    // 当 myA 和 myB 超出作用域时,它们会被正确销毁
    // A 的引用计数降为 0,A 销毁。
    // B 的引用计数降为 0,B 销毁。
    return 0;
}

通过将其中一方的

shared_ptr
替换为
weak_ptr
,我们打破了循环,确保了对象能够被正确销毁。

Visual Studio 2010使用方法 WORD文档 doc格式
Visual Studio 2010使用方法 WORD文档 doc格式

Visual Studio 2010使用方法 1 打开界面点击 文件---新建---项目 弹出新建项目界面,左边选择Visual C++,在右边选择空项目,然后在下面输入名称,存储位置,最后单击确定。之后会弹出界面(解决方案资源管理器)然后选择头文件,或源文件,单击右键---添加---新建项目 弹出 添加新项界面 左边 选择代码,右边选择 C++ 文件, 底部 输入名称,单击确定,之后会弹出新的界面,就可以编写

下载

std::make_shared
vs
new
:性能与异常安全的考量

在C++中创建

shared_ptr
时,你可能会看到两种常见的写法:

  1. std::shared_ptr p(new T(...));
  2. std::shared_ptr p = std::make_shared(...);

从表面上看,它们都实现了同样的目的,但

std::make_shared
在性能和异常安全性上有着显著的优势,我个人总是推荐使用它。

性能方面:

std::shared_ptr
内部需要维护一个控制块(control block),这个控制块包含了引用计数、
weak_ptr
计数以及可能的自定义删除器等信息。

  • 当使用
    new T()
    然后传递给
    shared_ptr
    构造函数时,会发生两次独立的内存分配:一次是为
    T
    对象本身分配内存,另一次是为
    shared_ptr
    的控制块分配内存。这两次分配可能会导致内存碎片,并且由于是两次系统调用,效率通常较低。
  • std::make_shared
    则非常聪明,它会尝试进行单次内存分配。它在一个连续的内存块中同时为
    T
    对象和
    shared_ptr
    的控制块分配空间。这不仅减少了内存分配的次数,提高了效率,还有助于改善缓存局部性(cache locality),因为对象和其管理信息存储在一起,CPU访问时效率更高。

异常安全性方面: 考虑一个表达式,比如

func(std::shared_ptr(new A()), std::shared_ptr(new B()));
在C++11/14标准中,编译器可能会以任意顺序执行子表达式。一个可能的执行顺序是:

  1. new A()
  2. new B()
  3. std::shared_ptr(ptr_A)
  4. std::shared_ptr(ptr_B)

如果

new A()
成功,但紧接着
new B()
抛出了异常,那么
ptr_A
指向的内存将永远不会被
std::shared_ptr
接管,从而导致
A
对象的内存泄漏。这种情况下,
shared_ptr
的构造函数还没来得及执行,它就无法管理这块内存了。

而使用

std::make_shared
则不会有这个问题:
func(std::make_shared(), std::make_shared());
如果
std::make_shared()
成功,但
std::make_shared()
抛出异常,那么
std::make_shared()
返回的
shared_ptr
会立即被销毁,其内部的
A
对象也会随之被正确释放。这是因为
make_shared
的整个操作是原子的,要么全部成功,要么在失败时能保证已分配资源的正确清理。

因此,除非你需要自定义删除器,或者需要从一个已经存在的裸指针来创建

shared_ptr
(例如,从一个C风格API返回的指针),否则
std::make_shared
几乎总是更优的选择。

std::shared_ptr
在多线程环境下的安全边界

std::shared_ptr
在多线程环境下的行为是一个经常被误解的话题。我见过不少开发者认为只要用了
shared_ptr
,所有关于线程安全的问题就都解决了,这其实是个危险的误区。理解
shared_ptr
的线程安全边界至关重要。

shared_ptr
自身的线程安全:
std::shared_ptr
的引用计数是线程安全的。这意味着,多个线程可以同时对同一个
shared_ptr
对象进行拷贝、赋值、销毁操作(这会导致引用计数的增减),这些操作都是原子性的。
标准库保证了这些引用计数的修改是正确的,不会出现竞态条件导致引用计数混乱。例如:

std::shared_ptr global_res = std::make_shared("Shared Data");

void thread_func() {
    std::shared_ptr local_res = global_res; // 引用计数安全地增加
    // ... 使用 local_res ...
} // local_res 销毁,引用计数安全地减少

在这种情况下,

global_res
的引用计数在多个线程中被安全地操作。

被管理对象的线程安全: 然而,

std::shared_ptr
不保证它所管理的对象的线程安全。如果多个线程通过不同的
shared_ptr
实例同时访问或修改同一个被管理的对象,你仍然需要自己实现同步机制(例如互斥锁
std::mutex
)。
shared_ptr
只负责对象的生命周期管理,而对对象内部数据的并发访问控制,则完全是另一回事。

举个例子:

#include 
#include 
#include 
#include 
#include 

class Counter {
public:
    int value = 0;
    std::mutex mtx; // 用于保护 value

    void increment() {
        std::lock_guard lock(mtx);
        value++;
    }
};

std::shared_ptr shared_counter = std::make_shared();

void worker_thread() {
    for (int i = 0; i < 1000; ++i) {
        shared_counter->increment(); // 访问被 shared_ptr 管理的对象
    }
}

int main() {
    std::vector threads;
    for (int i = 0; i < 10; ++i) {
        threads.emplace_back(worker_thread);
    }

    for (auto& t : threads) {
        t.join();
    }

    std::cout << "Final counter value: " << shared_counter->value << std::endl;
    return 0;
}

在这个例子中,

shared_ptr
确保了
Counter
对象的生命周期,但
Counter
内部的
value
成员变量的并发访问仍然需要
std::mutex
来保护。如果没有
mtx
value
的最终结果将是不确定的。

shared_ptr
本身的并发访问: 如果你在多个线程中对同一个
shared_ptr
实例(而不是它所指向的对象)进行读写操作,比如一个线程把
shared_ptr
赋值给另一个
shared_ptr
,另一个线程同时又给这个
shared_ptr
赋了新值,那么
shared_ptr
本身也需要保护。标准库提供了
std::atomic_load
std::atomic_store
等函数模板来原子地操作
shared_ptr
,但通常情况下,我们更倾向于通过互斥锁来保护对
shared_ptr
实例的并发修改,以避免复杂性。

总结来说,

shared_ptr
的引用计数是线程安全的,这解决了对象的生命周期管理问题。但当你通过
shared_ptr
访问其内部的对象数据时,如果这些数据可能被多个线程并发修改,你仍然需要传统的同步机制来保证数据的一致性和正确性。将
shared_ptr
视为一个智能的生命周期管理器,而不是一个万能的线程安全工具,这一点非常重要。

相关专题

更多
treenode的用法
treenode的用法

​在计算机编程领域,TreeNode是一种常见的数据结构,通常用于构建树形结构。在不同的编程语言中,TreeNode可能有不同的实现方式和用法,通常用于表示树的节点信息。更多关于treenode相关问题详情请看本专题下面的文章。php中文网欢迎大家前来学习。

535

2023.12.01

C++ 高效算法与数据结构
C++ 高效算法与数据结构

本专题讲解 C++ 中常用算法与数据结构的实现与优化,涵盖排序算法(快速排序、归并排序)、查找算法、图算法、动态规划、贪心算法等,并结合实际案例分析如何选择最优算法来提高程序效率。通过深入理解数据结构(链表、树、堆、哈希表等),帮助开发者提升 在复杂应用中的算法设计与性能优化能力。

17

2025.12.22

深入理解算法:高效算法与数据结构专题
深入理解算法:高效算法与数据结构专题

本专题专注于算法与数据结构的核心概念,适合想深入理解并提升编程能力的开发者。专题内容包括常见数据结构的实现与应用,如数组、链表、栈、队列、哈希表、树、图等;以及高效的排序算法、搜索算法、动态规划等经典算法。通过详细的讲解与复杂度分析,帮助开发者不仅能熟练运用这些基础知识,还能在实际编程中优化性能,提高代码的执行效率。本专题适合准备面试的开发者,也适合希望提高算法思维的编程爱好者。

21

2026.01.06

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

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

482

2023.08.10

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

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

143

2025.12.24

空指针异常处理
空指针异常处理

本专题整合了空指针异常解决方法,阅读专题下面的文章了解更多详细内容。

22

2025.11.16

数据库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、事务处理。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

210

2023.12.29

Java JVM 原理与性能调优实战
Java JVM 原理与性能调优实战

本专题系统讲解 Java 虚拟机(JVM)的核心工作原理与性能调优方法,包括 JVM 内存结构、对象创建与回收流程、垃圾回收器(Serial、CMS、G1、ZGC)对比分析、常见内存泄漏与性能瓶颈排查,以及 JVM 参数调优与监控工具(jstat、jmap、jvisualvm)的实战使用。通过真实案例,帮助学习者掌握 Java 应用在生产环境中的性能分析与优化能力。

19

2026.01.20

热门下载

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

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
C# 教程
C# 教程

共94课时 | 7.1万人学习

C 教程
C 教程

共75课时 | 4.1万人学习

C++教程
C++教程

共115课时 | 13万人学习

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

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