C++ 智能指针深度解析:std::make_shared 为何是最佳实践?
一、前言
在C++11引入的智能指针家族中,std::shared_ptr是最常用的共享所有权指针。然而,很多开发者对其构造方式的差异缺乏深入理解。本文将揭示std::make_shared与直接new构造的本质区别——这不仅是"一行代码"的简洁性问题,更关乎内存布局、异常安全和缓存性能。
二、内存分配机制:一次 vs 两次
2.1std::shared_ptr的内部结构
std::shared_ptr采用引用计数技术,其内部包含两个核心指针:
┌─────────────────────────────────────┐ │ std::shared_ptr<T> │ ├─────────────────┬───────────────────┤ │ T* ptr │ ControlBlock* cb │ │ (指向数据) │ (指向控制块) │ └─────────────────┴───────────────────┘控制块(Control Block)包含:
shared_count:强引用计数(shared_ptr数量)weak_count:弱引用计数(weak_ptr数量)删除器(Deleter)
分配器(Allocator)
2.2 两种构造方式的内存布局对比
方式一:直接new(两次分配)
#include <iostream> #include <memory> #include <chrono> // 模拟一个资源密集型对象 class DataBuffer { public: explicit DataBuffer(size_t size) : size_(size), data_(new int[size]) { std::cout << "DataBuffer 构造, size=" << size << "\n"; } ~DataBuffer() { std::cout << "DataBuffer 析构\n"; delete[] data_; } private: size_t size_; int* data_; }; void demonstrateTwoAllocations() { std::cout << "=== 方式一:两次内存分配 ===\n"; // 步骤拆解(概念上): // 1. new DataBuffer(1024) -> 分配 DataBuffer 内存(第1次) // 2. new ControlBlock -> 分配控制块内存(第2次) std::shared_ptr<DataBuffer> ptr(new DataBuffer(1024)); std::cout << "use_count: " << ptr.use_count() << "\n"; } int main() { demonstrateTwoAllocations(); return 0; }内存布局:
堆内存区域(不连续): ┌─────────────────┐ ┌──────────────────────────┐ │ Control Block │ │ DataBuffer │ │ (24 bytes) │ │ (size_ + data_指针) │ │ refcount=1 │ │ data_ -> [1024个int] │ └─────────────────┘ └──────────────────────────┘ ↑ ↑ └──────────┬───────────────┘ │ shared_ptr 持有两个指针方式二:std::make_shared(一次分配)
#include <iostream> #include <memory> class NetworkPacket { public: NetworkPacket(uint32_t id, size_t payloadSize) : packetId_(id), payload_(new char[payloadSize]), payloadSize_(payloadSize) { std::cout << "NetworkPacket 构造, id=" << id << "\n"; } ~NetworkPacket() { std::cout << "NetworkPacket 析构, id=" << packetId_ << "\n"; delete[] payload_; } private: uint32_t packetId_; char* payload_; size_t payloadSize_; }; void demonstrateOneAllocation() { std::cout << "\n=== 方式二:单次内存分配 ===\n"; // make_shared 一次性分配连续内存: // [Control Block | NetworkPacket 对象] auto ptr = std::make_shared<NetworkPacket>(1001, 4096); std::cout << "use_count: " << ptr.use_count() << "\n"; } int main() { demonstrateOneAllocation(); return 0; }内存布局:
堆内存区域(连续): ┌─────────────────────────────────────────────────────┐ │ Control Block │ NetworkPacket 对象 │ │ (24 bytes) │ packetId_ | payload_ | payloadSize_ │ │ refcount=1 │ payload_ -> [4096 bytes] │ └─────────────────────────────────────────────────────┘ ↑ │ shared_ptr 只需一个指针偏移即可访问数据三、异常安全:隐藏的内存泄漏陷阱
3.1 问题场景:函数参数求值顺序
C++标准不规定函数参数的求值顺序,这导致直接new构造shared_ptr存在严重的异常安全风险。
#include <iostream> #include <memory> #include <stdexcept> class DatabaseConnection { public: DatabaseConnection() { std::cout << "DB连接建立\n"; } ~DatabaseConnection() { std::cout << "DB连接断开\n"; } }; class CacheClient { public: CacheClient() { std::cout << "缓存客户端创建\n"; } ~CacheClient() { std::cout << "缓存客户端销毁\n"; } }; // 模拟可能抛出异常的构造 class RpcChannel { public: RpcChannel() { std::cout << "RPC通道创建\n"; // 模拟网络异常 if (rand() % 2 == 0) { throw std::runtime_error("网络连接超时"); } } ~RpcChannel() { std::cout << "RPC通道销毁\n"; } }; void initializeService( std::shared_ptr<DatabaseConnection> db, std::shared_ptr<CacheClient> cache, std::shared_ptr<RpcChannel> rpc ) { std::cout << "服务初始化完成\n"; } // 危险代码:可能导致内存泄漏 void dangerousInitialization() { std::cout << "\n=== 危险:直接 new 构造 ===\n"; try { // 编译器可能按以下顺序执行: // 1. new DatabaseConnection() // 2. new CacheClient() // 3. new RpcChannel() <-- 这里抛异常! // 4. shared_ptr 构造(第1个) // 5. shared_ptr 构造(第2个) // 6. shared_ptr 构造(第3个) <-- 永远不会执行 // 如果第3步抛异常,第1、2步的裸指针将永远丢失! initializeService( std::shared_ptr<DatabaseConnection>(new DatabaseConnection()), std::shared_ptr<CacheClient>(new CacheClient()), std::shared_ptr<RpcChannel>(new RpcChannel()) ); } catch (const std::exception& e) { std::cout << "捕获异常: " << e.what() << "\n"; std::cout << "注意:前面 new 的对象可能已泄漏!\n"; } } // 安全代码:使用 make_shared void safeInitialization() { std::cout << "\n=== 安全:make_shared 构造 ===\n"; try { // make_shared 是原子操作:要么完全成功,要么完全不分配 initializeService( std::make_shared<DatabaseConnection>(), std::make_shared<CacheClient>(), std::make_shared<RpcChannel>() ); } catch (const std::exception& e) { std::cout << "捕获异常: " << e.what() << "\n"; std::cout << "保证:没有内存泄漏!\n"; } } int main() { srand(time(nullptr)); dangerousInitialization(); safeInitialization(); return 0; }3.2 异常安全的本质原因
| 构造方式 | 异常安全级别 | 原因 |
|---|---|---|
shared_ptr<T>(new T()) | 基本保证 | new T()和new ControlBlock()是两个独立操作,中间可能抛异常 |
make_shared<T>() | 强保证 | 内存分配和对象构造在一个原子步骤中完成,失败时不泄漏 |
四、性能优势:缓存局部性
4.1 连续内存的缓存友好性
std::make_shared将控制块和数据对象放在同一块连续内存中,这带来了显著的缓存性能提升。
#include <iostream> #include <memory> #include <vector> #include <chrono> // 模拟高频访问的小对象 struct TelemetryPoint { double timestamp; double value; uint32_t sensorId; }; // 使用 make_shared 的批量创建 void benchmarkMakeShared(size_t count) { std::vector<std::shared_ptr<TelemetryPoint>> points; points.reserve(count); auto start = std::chrono::high_resolution_clock::now(); for (size_t i = 0; i < count; ++i) { points.push_back(std::make_shared<TelemetryPoint>( TelemetryPoint{static_cast<double>(i), i * 0.1, static_cast<uint32_t>(i % 100)} )); } // 模拟访问:遍历所有点(测试缓存局部性) volatile double sum = 0; for (const auto& pt : points) { sum += pt->value; } auto end = std::chrono::high_resolution_clock::now(); auto ms = std::chrono::duration<double, std::milli>(end - start).count(); std::cout << "make_shared: " << ms << " ms\n"; } // 使用 new 的批量创建 void benchmarkNew(size_t count) { std::vector<std::shared_ptr<TelemetryPoint>> points; points.reserve(count); auto start = std::chrono::high_resolution_clock::now(); for (size_t i = 0; i < count; ++i) { points.push_back(std::shared_ptr<TelemetryPoint>( new TelemetryPoint{static_cast<double>(i), i * 0.1, static_cast<uint32_t>(i % 100)} )); } volatile double sum = 0; for (const auto& pt : points) { sum += pt->value; } auto end = std::chrono::high_resolution_clock::now(); auto ms = std::chrono::duration<double, std::milli>(end - start).count(); std::cout << "new: " << ms << " ms\n"; } int main() { const size_t N = 1000000; std::cout << "创建 " << N << " 个 shared_ptr<TelemetryPoint>\n"; benchmarkMakeShared(N); benchmarkNew(N); return 0; }性能差异分析:
| 指标 | make_shared | new |
|---|---|---|
| 内存分配次数 | 1次/对象 | 2次/对象 |
| 内存碎片 | 少(连续) | 多(分散) |
| CPU缓存命中率 | 高(控制块+数据相邻) | 低(可能跨缓存行) |
| 分配器开销 | 低(单次malloc) | 高(两次malloc) |
五、make_shared的局限性
5.1 控制块生命周期延长问题
make_shared的唯一缺点是:控制块与数据对象共享同一块内存,只有当所有shared_ptr和weak_ptr都销毁后,整块内存才能释放。
#include <iostream> #include <memory> class HeavyResource { public: HeavyResource() { std::cout << "HeavyResource 构造\n"; } ~HeavyResource() { std::cout << "HeavyResource 析构\n"; } char buffer[1024 * 1024]; // 1MB 数据 }; void demonstrateWeakPtrIssue() { std::cout << "\n=== make_shared 与 weak_ptr 的内存延迟释放 ===\n"; std::weak_ptr<HeavyResource> weakRef; { // 使用 make_shared:控制块 + 1MB 数据 在同一块内存 auto shared = std::make_shared<HeavyResource>(); weakRef = shared; // 创建弱引用 std::cout << "shared_ptr 离开作用域...\n"; } // shared_ptr 销毁,但 weak_ptr 仍引用控制块! // 此时:HeavyResource 的析构函数已调用(因为 shared_count=0) // 但是:1MB 的内存仍未释放,因为控制块被 weak_ptr 持有! std::cout << "weak_ptr 仍有效? " << !weakRef.expired() << "\n"; std::cout << "注意:1MB 内存被 weak_ptr 的控制块占用,无法归还系统!\n"; weakRef.reset(); // 释放 weak_ptr,整块内存才真正释放 std::cout << "weak_ptr 重置后,内存完全释放\n"; } // 对比:使用 new 构造时,weak_ptr 不阻止数据内存释放 void demonstrateNewWithWeakPtr() { std::cout << "\n=== new 构造时 weak_ptr 的行为 ===\n"; std::weak_ptr<HeavyResource> weakRef; { // 两次分配:控制块 和 数据 分离 std::shared_ptr<HeavyResource> shared(new HeavyResource()); weakRef = shared; std::cout << "shared_ptr 离开作用域...\n"; } // 数据内存(1MB)立即释放!控制块(几十字节)被 weak_ptr 保留 std::cout << "数据内存已释放,仅保留小型控制块\n"; std::cout << "weak_ptr 仍有效? " << !weakRef.expired() << "\n"; weakRef.reset(); } int main() { demonstrateWeakPtrIssue(); demonstrateNewWithWeakPtr(); return 0; }5.2 何时应该使用new?
| 场景 | 建议 | 原因 |
|---|---|---|
| 常规对象管理 | ✅make_shared | 性能更好、异常安全 |
| 需要自定义删除器 | ❌new | make_shared不支持自定义删除器 |
对象极大且需weak_ptr | ❌new | 避免控制块延长大数据内存生命周期 |
| 使用私有构造函数 | ❌new+std::shared_ptr | make_shared无法访问私有构造函数 |
| 数组类型 | ❌new | C++20前make_shared不支持数组 |
六、最佳实践总结
6.1 代码规范
#include <memory> class Engine { public: Engine(int power) : power_(power) {} private: int power_; }; // ✅ 推荐:总是优先使用 make_shared auto createEngineSafe(int power) { return std::make_shared<Engine>(power); } // ❌ 避免:直接 new(除非有特殊需求) auto createEngineUnsafe(int power) { return std::shared_ptr<Engine>(new Engine(power)); // 不要这样做 } // ✅ 自定义删除器的正确方式(必须 new) auto createFileHandle(const char* path) { return std::shared_ptr<FILE>( fopen(path, "r"), [](FILE* fp) { if (fp) fclose(fp); } ); }6.2 决策树
需要 shared_ptr? ├── 是 │ ├── 需要自定义删除器? ──> 使用 new │ ├── 需要 weak_ptr 且对象极大? ──> 考虑 new │ ├── 构造函数私有? ──> 使用 new(配合友元) │ └── 否则 ──> ✅ 优先使用 make_shared └── 否 └── 考虑 unique_ptr 或原始指针七、总结
| 对比维度 | std::make_shared | std::shared_ptr(new T) |
|---|---|---|
| 内存分配 | 1次(原子) | 2次(分离) |
| 异常安全 | 强保证 | 基本保证(有泄漏风险) |
| 缓存性能 | 优(局部性好) | 差(可能跨页) |
| 内存开销 | 可能延迟释放(weak_ptr) | 及时释放 |
| 代码简洁 | 优 | 差 |
| 灵活性 | 不支持自定义删除器 | 支持 |
核心原则:除非有特殊需求(自定义删除器、极大对象+weak_ptr、私有构造函数),否则始终使用std::make_shared。
