当前位置: 首页 > news >正文

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_sharednew
内存分配次数1次/对象2次/对象
内存碎片少(连续)多(分散)
CPU缓存命中率高(控制块+数据相邻)低(可能跨缓存行)
分配器开销低(单次malloc)高(两次malloc)

五、make_shared的局限性

5.1 控制块生命周期延长问题

make_shared的唯一缺点是:控制块与数据对象共享同一块内存,只有当所有shared_ptrweak_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性能更好、异常安全
需要自定义删除器newmake_shared不支持自定义删除器
对象极大且需weak_ptrnew避免控制块延长大数据内存生命周期
使用私有构造函数new+std::shared_ptrmake_shared无法访问私有构造函数
数组类型newC++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_sharedstd::shared_ptr(new T)
内存分配1次(原子)2次(分离)
异常安全强保证基本保证(有泄漏风险)
缓存性能优(局部性好)差(可能跨页)
内存开销可能延迟释放(weak_ptr)及时释放
代码简洁
灵活性不支持自定义删除器支持

核心原则:除非有特殊需求(自定义删除器、极大对象+weak_ptr、私有构造函数),否则始终使用std::make_shared

http://www.jsqmd.com/news/818018/

相关文章:

  • 2026白山市黄金回收白银回收铂金回收店铺实力排行榜TOP5; K金+金条+银条+首饰回收靠谱门店及联系方式推荐_转自TXT - 盛世金银回收
  • FilterDiff——用于加速MRI重建的无噪声频域扩散模型
  • 拳心向暖,大爱无声——奥运冠军蔡良蝉的公益坚守
  • 2026白银市会宁县黄金回收白银回收铂金回收店铺实力排行榜TOP5; K金+金条+银条+首饰回收靠谱门店及联系方式推荐_转自TXT - 盛世金银回收
  • NotebookLM隐私策略2024年4月重大更新:新增“仅本地处理”模式?我们逆向了v2.3.1前端代码(独家)
  • USB IP设计演进与FinFET工艺挑战解析
  • 别再只盯着YOLO了!2024年目标检测实战选型指南:从NanoDet到DETR,谁才是你的菜?
  • 3步解锁自动化:Elsevier Tracker智能追踪工具完全指南
  • 如何快速掌握OpenCore配置:3步搞定黑苹果引导的完整指南
  • 从GDC题解到实战:算法竞赛中的经典模型与破局思路
  • 别再死记硬背了!用Python写个八字神煞自动查询工具(附完整源码)
  • LLM长序列服务优化:LServe的块稀疏注意力技术
  • 2026白银市景泰县黄金回收白银回收铂金回收店铺实力排行榜TOP5; K金+金条+银条+首饰回收靠谱门店及联系方式推荐_转自TXT - 盛世金银回收
  • AI 与钓鱼即服务重构电子邮件威胁格局及防御体系研究
  • Spring事务失效?8个高频隐形坑+代码实操,面试说透直接加分
  • ABAP实战避坑:FIELD-SYMBOLS指针搭配FOR ALL ENTRIES IN的正确姿势,你写对了吗?
  • AI原生内核升级,移动云大云海山数据库筑牢企业数智底座
  • 如何用WinUtil在5分钟内完成Windows系统优化和软件安装?
  • 从ARM到DSP:手把手拆解嵌入式CPU的哈佛结构与RISC指令集,搞定软考硬件大题
  • 容联云:为城商行打造“企业级大运营体系”的实践路径
  • SDR++ 终极指南:跨平台软件定义无线电快速精通
  • 合肥招聘信息最新招聘有哪些,以及平台! - drfdxr
  • 从LiDAR扫描到三维模型:手把手教你用CloudCompare完成点云全流程处理
  • 图解人工智能(15)基于知识的人工智能
  • 移动机器人从“可用“到“好用“的工业级跨越
  • 3分钟拯救你的B站收藏:m4s视频转换终极解决方案
  • 2026白银市靖远县黄金回收白银回收铂金回收店铺实力排行榜TOP5; K金+金条+银条+首饰回收靠谱门店及联系方式推荐_转自TXT - 盛世金银回收
  • wechatapi iPad协议,让微信二次开发飞起来
  • 【OpenClaw全面解析:从零到精通】第53篇:OpenClaw多模态能力应用实战:Computer Use Agent、Peekaboo v3视觉自动化与语音交互完整指南
  • 在裁员和招聘同步进行的市场里,这样的技术人才永远不缺Offer