资讯动态

C++ 智能指针深度解析:std::make_shared 为何是最佳实践?

发布时间:2026/8/19 21:57:06 来源:尧图企业网站定制
一、前言在C11引入的智能指针家族中std::shared_ptr是最常用的共享所有权指针。然而很多开发者对其构造方式的差异缺乏深入理解。本文将揭示std::make_shared与直接new构造的本质区别——这不仅是一行代码的简洁性问题更关乎内存布局、异常安全和缓存性能。二、内存分配机制一次 vs 两次2.1std::shared_ptr的内部结构std::shared_ptr采用引用计数技术其内部包含两个核心指针┌─────────────────────────────────────┐ │ std::shared_ptrT │ ├─────────────────┬───────────────────┤ │ T* ptr │ ControlBlock* cb │ │ (指向数据) │ (指向控制块) │ └─────────────────┴───────────────────┘控制块Control Block包含shared_count强引用计数shared_ptr数量weak_count弱引用计数weak_ptr数量删除器Deleter分配器Allocator2.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_ptrDataBuffer ptr(new DataBuffer(1024)); std::cout use_count: ptr.use_count() \n; } int main() { demonstrateTwoAllocations(); return 0; }内存布局堆内存区域不连续 ┌─────────────────┐ ┌──────────────────────────┐ │ Control Block │ │ DataBuffer │ │ (24 bytes) │ │ (size_ data_指针) │ │ refcount1 │ │ 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_sharedNetworkPacket(1001, 4096); std::cout use_count: ptr.use_count() \n; } int main() { demonstrateOneAllocation(); return 0; }内存布局堆内存区域连续 ┌─────────────────────────────────────────────────────┐ │ Control Block │ NetworkPacket 对象 │ │ (24 bytes) │ packetId_ | payload_ | payloadSize_ │ │ refcount1 │ 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_ptrDatabaseConnection db, std::shared_ptrCacheClient cache, std::shared_ptrRpcChannel 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_ptrDatabaseConnection(new DatabaseConnection()), std::shared_ptrCacheClient(new CacheClient()), std::shared_ptrRpcChannel(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_sharedDatabaseConnection(), std::make_sharedCacheClient(), std::make_sharedRpcChannel() ); } 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_ptrT(new T())基本保证new T()和new ControlBlock()是两个独立操作中间可能抛异常make_sharedT()强保证内存分配和对象构造在一个原子步骤中完成失败时不泄漏四、性能优势缓存局部性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::vectorstd::shared_ptrTelemetryPoint 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_sharedTelemetryPoint( TelemetryPoint{static_castdouble(i), i * 0.1, static_castuint32_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::durationdouble, std::milli(end - start).count(); std::cout make_shared: ms ms\n; } // 使用 new 的批量创建 void benchmarkNew(size_t count) { std::vectorstd::shared_ptrTelemetryPoint 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_ptrTelemetryPoint( new TelemetryPoint{static_castdouble(i), i * 0.1, static_castuint32_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::durationdouble, std::milli(end - start).count(); std::cout new: ms ms\n; } int main() { const size_t N 1000000; std::cout 创建 N 个 shared_ptrTelemetryPoint\n; benchmarkMakeShared(N); benchmarkNew(N); return 0; }性能差异分析指标make_sharednew内存分配次数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_ptrHeavyResource weakRef; { // 使用 make_shared控制块 1MB 数据 在同一块内存 auto shared std::make_sharedHeavyResource(); weakRef shared; // 创建弱引用 std::cout shared_ptr 离开作用域...\n; } // shared_ptr 销毁但 weak_ptr 仍引用控制块 // 此时HeavyResource 的析构函数已调用因为 shared_count0 // 但是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_ptrHeavyResource weakRef; { // 两次分配控制块 和 数据 分离 std::shared_ptrHeavyResource 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性能更好、异常安全需要自定义删除器❌newmake_shared不支持自定义删除器对象极大且需weak_ptr❌new避免控制块延长大数据内存生命周期使用私有构造函数❌newstd::shared_ptrmake_shared无法访问私有构造函数数组类型❌newC20前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_sharedEngine(power); } // ❌ 避免直接 new除非有特殊需求 auto createEngineUnsafe(int power) { return std::shared_ptrEngine(new Engine(power)); // 不要这样做 } // ✅ 自定义删除器的正确方式必须 new auto createFileHandle(const char* path) { return std::shared_ptrFILE( fopen(path, r), [](FILE* fp) { if (fp) fclose(fp); } ); }6.2 决策树需要 shared_ptr? ├── 是 │ ├── 需要自定义删除器? ── 使用 new │ ├── 需要 weak_ptr 且对象极大? ── 考虑 new │ ├── 构造函数私有? ── 使用 new配合友元 │ └── 否则 ── ✅ 优先使用 make_shared └── 否 └── 考虑 unique_ptr 或原始指针七、总结对比维度std::make_sharedstd::shared_ptr(new T)内存分配1次原子2次分离异常安全强保证基本保证有泄漏风险缓存性能优局部性好差可能跨页内存开销可能延迟释放weak_ptr及时释放代码简洁优差灵活性不支持自定义删除器支持核心原则除非有特殊需求自定义删除器、极大对象weak_ptr、私有构造函数否则始终使用std::make_shared。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价