Effective C 条款17以独立语句将 newed 对象置入智能指针核心观点以独立语句将 newed 对象存储于置入智能指针内。如果不这样做一旦异常被抛出可能导致难以察觉的资源泄漏。一、一个看似无害的陷阱让我们从一个常见的函数调用开始#includememory#includeiostreamclassWidget{public:Widget(){std::coutWidget 构造\n;}~Widget(){std::coutWidget 析构\n;}};voidprocessWidget(std::shared_ptrWidgetpw,intpriority);intcomputePriority();// 看似合理的调用方式voiddoWork(){processWidget(std::shared_ptrWidget(newWidget()),computePriority());}这段代码有问题吗表面上看new Widget()的结果被立即传入了shared_ptr的构造函数然后传给processWidget。但这里隐藏着一个致命的异常安全隐患。二、编译器视角操作序列的不确定性2.1 C 的求值顺序在深入问题之前我们需要理解一个关键事实⚠️C 标准不保证函数参数之间的求值顺序直到 C17 才对某些情况做出规定。在 C17 之前编译器在编译processWidget(std::shared_ptrWidget(new Widget()), computePriority())时以下操作的执行顺序是不确定的执行new Widget()调用std::shared_ptrWidget的构造函数调用computePriority()编译器可能按照以下顺序执行这是合法的1. new Widget() → 返回裸指针 2. computePriority() → 可能抛出异常 3. shared_ptr 构造 → 如果步骤2抛异常这一步永远不会执行2.2 灾难场景还原假设computePriority()在运行时抛出了异常intcomputePriority(){// 某些条件下抛出异常throwstd::runtime_error(优先级计算失败);return42;}voiddoWork(){// 潜在的资源泄漏processWidget(std::shared_ptrWidget(newWidget()),computePriority());}执行流程可能是步骤操作结果1new Widget()分配内存调用构造函数返回裸指针2computePriority()抛出异常3shared_ptr构造永远不会执行此时new Widget()返回的裸指针永远丢失了——没有智能指针接管它也没有代码能够delete它。这就是资源泄漏。 这种泄漏特别危险因为代码看起来正确编译器不会报错只有在特定异常触发时才会发生极难在测试中发现三、解决方案独立语句3.1 正确的写法将new和智能指针的构造放在一条独立的语句中voiddoWorkSafe(){// ✅ 独立语句newed 对象立即被置入智能指针std::shared_ptrWidgetpw(newWidget());// 现在即使 computePriority() 抛异常pw 的析构函数也会正确释放 WidgetprocessWidget(pw,computePriority());}为什么这样更安全因为 C 保证在一条语句执行完毕之前编译器不会插入其他操作。一旦std::shared_ptrWidget pw(new Widget());这条语句完成Widget对象就已经被智能指针安全接管了。3.2 C11 的更佳方案std::make_shared在 C11 及以后最佳实践是使用std::make_sharedvoiddoWorkBest(){// ✅ 最佳实践使用 make_shared更安全、更高效autopwstd::make_sharedWidget();processWidget(pw,computePriority());}std::make_shared的优势优势说明异常安全Widget的创建和shared_ptr的管理是原子操作性能更好只需一次内存分配控制块 对象在同一内存块中代码简洁不需要显式new防止泄漏彻底杜绝裸指针暴露的机会3.3 对比总结// ❌ 危险参数内部完成 new 和智能指针构造processWidget(std::shared_ptrWidget(newWidget()),computePriority());// ✅ 安全独立语句std::shared_ptrWidgetpw(newWidget());processWidget(pw,computePriority());// ✅✅ 最佳使用 make_sharedautopwstd::make_sharedWidget();processWidget(pw,computePriority());四、实际应用场景4.1 场景GUI 框架中的控件创建#includememory#includestdexceptclassButton{public:explicitButton(conststd::stringlabel){/* ... */}};classWindow{public:voidaddButton(std::shared_ptrButtonbtn,intzOrder);};intcalculateZOrder();// 可能抛出异常// ❌ 危险的控件添加voidsetupWindowDangerous(Windowwindow){window.addButton(std::shared_ptrButton(newButton(OK)),calculateZOrder());}// ✅ 安全的控件添加voidsetupWindowSafe(Windowwindow){autobtnstd::make_sharedButton(OK);window.addButton(btn,calculateZOrder());}4.2 场景数据库连接池#includememory#includestringclassDatabaseConnection{public:explicitDatabaseConnection(conststd::stringconnStr){// 建立连接可能分配大量资源}~DatabaseConnection(){// 关闭连接释放资源}};std::stringbuildConnectionString();// 可能因配置错误抛出异常classConnectionPool{public:voidaddConnection(std::shared_ptrDatabaseConnectionconn,intpriority);};// ❌ 危险如果 buildConnectionString 抛异常数据库连接泄漏voidinitializePoolDangerous(ConnectionPoolpool){pool.addConnection(std::shared_ptrDatabaseConnection(newDatabaseConnection(initial)),buildConnectionString().length());}// ✅ 安全分步执行voidinitializePoolSafe(ConnectionPoolpool){autoconnstd::make_sharedDatabaseConnection(initial);autoconnStrbuildConnectionString();// 即使这里抛异常conn 也会被正确释放pool.addConnection(conn,connStr.length());}4.3 场景多线程环境中的资源创建在多线程环境下这个问题更加微妙#includememory#includethread#includevectorclassTask{public:voidexecute();};intfetchTaskPriority();// 可能涉及网络请求可能抛异常// ❌ 在多线程中传递参数时尤其危险voiddispatchTaskDangerous(){std::threadt([](std::shared_ptrTasktask,intpriority){task-execute();},std::shared_ptrTask(newTask()),// 潜在的泄漏点fetchTaskPriority()// 可能抛异常);t.detach();}// ✅ 安全版本voiddispatchTaskSafe(){autotaskstd::make_sharedTask();intpriorityfetchTaskPriority();std::threadt([](std::shared_ptrTaskt,intp){t-execute();},task,priority);t.detach();}五、深入理解为什么编译器会这样5.1 编译器的优化自由C 标准赋予编译器很大的自由度来优化函数参数的求值顺序。这种设计是为了允许不同架构的最优代码生成支持各种编译器优化策略保持语言的灵活性但这也意味着程序员必须对序列点sequence pointsC11 后称为 sequenced-before 关系有清晰的理解。5.2 C17 的改进好消息是C17 引入了更严格的求值顺序规则在 C17 中函数参数的求值顺序仍然不完全确定但某些操作被保证为顺序执行。然而即使如此显式使用独立语句仍然是最佳实践因为它让代码意图更清晰兼容 C11/14 代码库避免依赖复杂的求值顺序规则更容易被代码审查者理解六、unique_ptr 同样适用虽然条款示例使用了shared_ptr但同样的原则完全适用于unique_ptr#includememoryclassResource{public:Resource()default;voidprocess();};voiduseResource(std::unique_ptrResourceres,intconfig);intloadConfig();// 可能抛异常// ❌ 危险voidprepareDangerous(){useResource(std::unique_ptrResource(newResource()),loadConfig());}// ✅ 安全voidprepareSafe(){autoresstd::make_uniqueResource();useResource(std::move(res),loadConfig());}注意unique_ptr不可拷贝所以传递时需要std::move。但创建它时仍然应该使用独立语句或std::make_uniqueC14 起可用。七、总结要点说明核心规则以独立语句将 newed 对象置入智能指针根本原因C 不保证函数参数的求值顺序潜在风险异常抛出时裸指针未被智能指针接管导致资源泄漏最佳实践使用std::make_shared/std::make_unique彻底避免裸指针兼容性独立语句写法兼容所有 C 版本一句话记住不要让new和智能指针的构造之间夹着可能抛异常的代码。要么用独立语句要么直接用make_shared/make_unique。八、延伸阅读Effective C 条款13以对象管理资源Effective C 条款16成对使用 new 和 delete 时要采取相同形式Effective C 条款18让接口容易被正确使用不易被误用C Core GuidelinesES.60 - Avoidnewanddeleteoutside resource management functions如果这篇文章对你有帮助欢迎点赞 、收藏 ⭐、评论 你的支持是我持续创作的动力