在 C++ 开发的世界里,管理动态内存一直是我们面临的核心挑战之一。随着现代 C++ 标准的演进,智能指针的出现极大地改善了我们的开发体验和代码安全性。而在众多智能指针工具中,std::shared_ptr 作为一种共享所有权的智能指针,被广泛应用于复杂的对象生命周期管理中。
但是,你是否思考过这样一个问题:在创建 INLINECODE6d8aa5f5 时,我们是直接使用 INLINECODE9262319f 表达式,还是使用标准库提供的 std::make_shared 工厂函数?
虽然这两种方式最终都能得到一个智能指针,但在性能、安全性以及代码的异常安全性上,它们之间存在着微妙的差异。在这篇文章中,我们将深入探讨 std::make_shared 的工作原理,分析它为何优于传统的构造方式,并通过丰富的代码示例,带你掌握如何在 C++ 项目中有效地利用这一工具来编写更高效、更健壮的代码。
什么 std::make_shared?
简单来说,INLINECODEc3280ae6 是 C++11 标准库 INLINECODE4376ab3a 头文件中引入的一个函数模板。它的主要作用是在单次操作中完成两件事:
- 在堆上分配内存以创建一个新对象。
- 创建一个管理该对象的
std::shared_ptr控制块(包括引用计数)。
它返回一个指向新创建对象的 INLINECODEe2002ce7。相比于手动使用 INLINECODE19e6b10f 关键字,推荐优先使用 std::make_shared,因为它不仅代码更简洁,而且在性能和异常安全性方面表现更佳。
语法与参数
std::make_shared 的基本语法非常直观:
template
std::shared_ptr std::make_shared( Args&&... args );
参数说明:
- T: 我们想要创建的对象的类型。
- args…: 传递给 T 类型构造函数的参数列表(支持完美转发)。
返回值:
- 一个
std::shared_ptr类型的对象,指向新创建的 T 类型实例。
为什么我们需要 std::make_shared?
你可能会问,既然我们已经有了 shared_ptr 的构造函数,可以直接像这样写代码:
std::shared_ptr ptr(new MyClass(10));
为什么还要多此一举引入 std::make_shared 呢?让我们从三个关键维度来剖析背后的原因。
1. 性能优化:减少内存分配次数
这是 std::make_shared 最大的优势。
使用构造函数(传统方式):
当我们执行 shared_ptr ptr(new int(10)) 时,内存分配实际上发生了两次:
- 第一次分配:执行 INLINECODE6a878212,在堆上分配内存存储 INLINECODE901bfc76 对象。
- 第二次分配:
shared_ptr的构造函数需要创建一个控制块,用于存储引用计数、弱引用计数和删除器等信息。这需要在堆上再次分配内存。
这意味着我们执行了两次堆内存分配操作,这在高并发或高性能敏感的场景下,可能会带来额外的开销。
使用 std::make_shared:
当我们执行 auto ptr = make_shared(10) 时,编译器会进行一项优化:
它将控制块和对象本身的内存分配合并为一次。也就是说,控制块和对象被分配在同一块连续的内存中。
- 优势: 减少了内存分配器的调用次数,提高了运行效率。
- 优势: 提高了内存局部性,因为对象和控制块在内存中是相邻的,CPU 缓存命中率更高。
2. 异常安全性
考虑以下情况:我们需要在一个函数调用中创建对象并传递给智能指针。假设我们有这样一个函数 INLINECODE6f41aa03,它接受一个 INLINECODE535234fe 和其他参数:
// 假设的函数
void processWidget(std::shared_ptr sp, int extraData);
不安全的调用方式:
“INLINECODEa029c469`INLINECODE23ba3880operator newINLINECODE2f4433d5operator newINLINECODEafaf6d23operator deleteINLINECODE07da2913makesharedINLINECODE22e902b4makesharedINLINECODE00582696std::makesharedINLINECODE53cb6cebsharedptrINLINECODE17099226sharedptrINLINECODE43818e58std::makeshared` 吗?”