1. shared_ptr 的两种创建方式:构造函数 vs make_shared
在 C++11 引入的智能指针体系中,shared_ptr 无疑是最常用的引用计数智能指针。但很多开发者并不清楚,创建 shared_ptr 实际上存在两种截然不同的方式:直接调用构造函数和使用 make_shared 工厂函数。这两种方式在内存布局、异常安全和性能表现上有着本质区别。
1.1 构造函数创建方式解析
当我们使用构造函数创建 shared_ptr 时,实际上发生了两次独立的内存分配:
cpp复制std::shared_ptr<MyClass> ptr(new MyClass());
这个看似简单的语句背后,隐藏着以下内存操作:
- 通过
new MyClass()在堆上分配对象内存 - 在 shared_ptr 构造函数内部,分配控制块(control block)内存
- 将对象指针和控制块关联
这种分离式分配带来了几个潜在问题:
- 内存碎片化:两次分配可能不在连续内存区域
- 异常安全问题:如果在第一步和第二步之间抛出异常,会导致内存泄漏
- 缓存不友好:对象数据和控制块可能位于不同内存页
关键提示:在 C++17 之前,构造函数创建方式不是异常安全的。如果控制块分配失败,已分配的 MyClass 对象会泄漏。
1.2 make_shared 的优化实现
相比之下,make_shared 采用了完全不同的实现策略:
cpp复制auto ptr = std::make_shared<MyClass>();
make_shared 的核心优化在于:
- 单次内存分配:同时预留对象和控制块所需空间
- 内存局部性:对象和控制块位于连续内存区域
- 内置异常安全:分配失败时会自动清理所有资源
根据标准库实现的不同,make_shared 通常会分配一个内存块,其布局大致如下:
code复制[控制块][对象内存]
这种布局带来了显著的性能优势:
- 减少内存分配次数(从2次降为1次)
- 提高缓存命中率(控制块和对象相邻)
- 降低内存碎片可能性
1.3 性能对比实测数据
为了量化两种方式的差异,我设计了以下测试场景:
cpp复制constexpr size_t
