C++智能指针:shared_ptr、make_shared与make_shared(new T)的关联与比较
在C++内存管理中,智能指针是一种重要的RAII(资源获取即初始化)机制,它能够自动管理动态分配的内存,避免内存泄漏。其中,std::shared_ptr是最常用的智能指针之一,而std::make_shared则是创建shared_ptr的推荐方式。本文将深入分析std::shared_ptr、make_shared与make_shared(new T)之间的关联、管理特点以及性能比较。
一、核心概念解析
1. std::shared_ptr:引用计数的智能指针
std::shared_ptr是C++11引入的共享所有权智能指针,其核心特性是:
- 引用计数:内部维护一个引用计数器,记录有多少个
shared_ptr实例指向同一个对象; - 自动析构:当引用计数降为0时,自动释放所管理的对象;
- 共享所有权:多个
shared_ptr可以同时拥有同一个对象的所有权; - 线程安全:引用计数的操作是线程安全的,但对象的访问需要手动同步。
2. std::make_shared:创建shared_ptr的推荐方式
std::make_shared是一个模板函数,用于创建shared_ptr实例,其核心优势是:
- 内存优化:将控制块(包含引用计数等元数据)和对象本身分配在同一块内存中,减少内存分配次数;
- 异常安全:避免了在创建对象和创建
shared_ptr之间发生异常导致的内存泄漏; - 代码简洁:语法更简洁,减少代码冗余。
3. make_shared(new T):不推荐的使用方式
make_shared(new T)这种写法虽然可以工作,但存在以下问题:
- 内存分配:会导致两次内存分配(一次用于对象,一次用于控制块);
- 异常安全:在某些情况下可能导致内存泄漏;
- 代码冗余:相比直接使用
make_shared<T>(),代码更冗长。
二、代码示例与解析
1. 三种方式的基本用法
1 |
|
运行结果:
1 | MyClass constructed with value: 1 |
从运行结果可以看出,方式3会导致对象被构造两次,析构三次,造成了不必要的开销和潜在的问题。
2. 内存分配对比
1 |
|
3. 异常安全对比
1 |
|
三、三种方式的详细比较
| 特性 | std::shared_ptr(new T) | std::make_shared |
std::make_shared |
|---|---|---|---|
| 内存分配次数 | 2次(对象+控制块) | 1次(对象+控制块) | 2次(对象+控制块),且对象被复制 |
| 异常安全性 | 部分安全 | 完全安全 | 不安全(可能内存泄漏) |
| 代码简洁性 | 较简洁 | 最简洁 | 最冗长 |
| 性能 | 较低 | 较高 | 最低 |
| 适用场景 | 需要自定义删除器时 | 一般场景推荐使用 | 不推荐使用 |
1. 内存分配差异
std::shared_ptr(new T):
- 第一次分配:为对象T分配内存
- 第二次分配:为控制块(包含引用计数、弱引用计数等)分配内存
- 优点:可以指定自定义删除器
- 缺点:两次内存分配,效率较低
std::make_shared
() :- 只分配一次内存,将对象和控制块放在同一块内存中
- 优点:减少内存分配次数,提高缓存局部性
- 缺点:无法指定自定义删除器
std::make_shared
(*new T*) :- 第一次分配:为临时对象分配内存
- 第二次分配:为
make_shared创建的对象和控制块分配内存 - 临时对象被复制到新分配的内存中
- 临时对象的内存泄漏风险
- 优点:无
- 缺点:多次内存分配,效率低,可能内存泄漏
2. 异常安全差异
std::shared_ptr(new T):
- 在以下情况下可能内存泄漏:
1
foo(std::shared_ptr<T>(new T()), bar()); // 如果bar()抛出异常,new T()的内存会泄漏
- 原因:参数评估顺序不确定,可能先执行
new T(),然后执行bar(),如果bar()抛出异常,shared_ptr构造函数不会被调用
- 在以下情况下可能内存泄漏:
std::make_shared
() :- 完全异常安全,因为对象创建和
shared_ptr构造在同一个函数调用中完成 - 即使在参数传递过程中发生异常,也不会内存泄漏
- 完全异常安全,因为对象创建和
**std::make_shared
(*new T)**: - 最不安全,因为临时对象的创建和
make_shared的调用是分离的 - 如果
make_shared内部发生异常,临时对象的内存会泄漏
- 最不安全,因为临时对象的创建和
3. 性能差异
- 内存分配:
make_shared只分配一次内存,比shared_ptr(new T)快 - 缓存局部性:
make_shared将对象和控制块放在同一块内存,提高缓存命中率 - 析构时间:
make_shared的控制块和对象在同一块内存,析构时可以一次性释放
四、使用建议与最佳实践
1. 优先使用std::make_shared
在大多数情况下,应优先使用std::make_shared,因为它:
- 更高效(一次内存分配)
- 更安全(异常安全)
- 代码更简洁
2. 仅在需要自定义删除器时使用std::shared_ptr(new T)
当需要指定自定义删除器时,必须使用std::shared_ptr的构造函数:
1 | // 使用自定义删除器 |
3. 绝对避免使用make_shared(new T)
这种写法不仅效率低下,还可能导致内存泄漏,应该完全避免。
4. 注意事项
- 循环引用:
shared_ptr可能导致循环引用,此时需要使用weak_ptr来打破循环 - 线程安全:
shared_ptr的引用计数操作是线程安全的,但对象的访问需要手动同步 - 大小:
shared_ptr的大小通常是原始指针的两倍(一个指向对象,一个指向控制块) - 自定义删除器:自定义删除器不会增加
shared_ptr的大小,但会影响类型
五、性能测试
1. 内存分配性能测试
1 |
|
2. 测试结果
在大多数现代系统上,make_shared的性能通常比shared_ptr(new T)快30-50%,主要原因是减少了内存分配次数和提高了缓存局部性。
六、总结
std::shared_ptr(new T):
- 适用场景:需要自定义删除器时
- 优点:灵活,可以指定自定义删除器
- 缺点:两次内存分配,可能存在异常安全问题
std::make_shared
() :- 适用场景:一般场景推荐使用
- 优点:一次内存分配,异常安全,代码简洁
- 缺点:无法指定自定义删除器
**std::make_shared
(*new T)**: - 适用场景:无
- 优点:无
- 缺点:多次内存分配,可能内存泄漏,效率低
在实际开发中,应优先使用std::make_shared,仅在需要自定义删除器时才使用std::shared_ptr(new T),绝对避免使用std::make_shared<T>(*new T)。
通过合理选择智能指针的创建方式,可以提高代码的性能、安全性和可维护性,避免内存泄漏等常见问题。

