
Effective C 条款26尽可能延后变量定义式的出现时间只要定义了一个变量而其类型带有一个构造函数或析构函数那么当程序的控制流到达这个变量定义式时你便得承受构造成本当这个变量离开其作用域时你便得承受析构成本。一、为什么要延后变量定义在 C 语言特别是 C89的旧习惯中开发者往往喜欢在函数开头把所有变量都定义好。这种风格在 C 中却可能带来不必要的性能开销。C 中的对象往往伴随着构造函数和析构函数的调用过早定义变量意味着不必要的构造开销变量在定义时就会调用构造函数不必要的析构开销即使变量未被使用离开作用域时也会调用析构函数代码清晰度下降读者需要跳过很多行才能看到变量真正被使用的地方核心原则不只应该延后变量的定义直到非得使用该变量的前一刻为止甚至应该尝试延后这份定义直到能够给它初值实参为止。二、代码示例对比反例过早定义变量#includestring#includestdexcept// 加密密码的函数std::stringencryptPassword(conststd::stringpassword){usingnamespacestd;// ❌ 过早定义如果下面抛出异常这个对象就白构造了string encrypted;if(password.length()8){throwlogic_error(Password is too short);// encrypted 在这里被构造后又立即被析构完全浪费}// 真正的加密操作constchar*keyMySecretKey;encryptedperformEncryption(password,key);returnencrypted;}正例延后到需要使用时才定义#includestring#includestdexceptstd::stringencryptPassword(conststd::stringpassword){usingnamespacestd;if(password.length()8){throwlogic_error(Password is too short);// 没有任何对象被无意义地构造和析构}// ✅ 延后定义此时确定会用到 encryptedconstchar*keyMySecretKey;string encryptedperformEncryption(password,key);returnencrypted;}更进一步直接以初值定义// ✅✅ 最佳实践定义时直接初始化std::stringencryptPassword(conststd::stringpassword){if(password.length()8){throwstd::logic_error(Password is too short);}// 直接以初值定义避免 default 构造后再赋值std::stringencrypted(performEncryption(password,MySecretKey));returnencrypted;}三、性能差异分析方式构造函数调用析构函数调用赋值操作效率评级过早定义 后续赋值1 次 default1 次1 次⭐⭐延后定义 后续赋值1 次 default1 次1 次⭐⭐⭐延后定义 直接初始化1 次 copy/移动1 次0 次⭐⭐⭐⭐⭐对于std::string这类带有动态内存分配的类型default 构造后再赋值的成本远高于直接以初值构造循环中的变量定义// 方式A变量定义在循环外Widget w;for(inti0;in;i){w取决于i的某个值;// 使用 w}// 1 次构造 1 次析构 n 次赋值// 方式B变量定义在循环内for(inti0;in;i){Widgetw(取决于i的某个值);// 使用 w}// n 次构造 n 次析构如何选择如果赋值成本低于 构造析构 成本 → 选择方式A如果构造析构成本低于赋值成本 → 选择方式B对于大部分 C 类型尤其是 STL 容器、string 等→通常方式B更优四、实际应用场景场景1文件处理#includefstream#includestringvoidprocessFile(conststd::stringfilepath){// ❌ 不好的做法std::ifstream file;std::string line;std::string content;if(filepath.empty()){return;// 三个对象都被无意义地构造和析构了}file.open(filepath);// ...// ✅ 好的做法if(filepath.empty()){return;// 没有任何对象被创建}std::ifstreamfile(filepath);// 需要时才创建std::string line;std::string content;// ...}场景2数据库查询#includemysql/mysql.hclassQueryResult{public:QueryResult(MYSQL_RES*res):result(res){}~QueryResult(){if(result)mysql_free_result(result);}// ...private:MYSQL_RES*result;};voidfetchUserData(intuserId){// ❌ 过早定义QueryResultresult(nullptr);MYSQL*conngetConnection();if(userId0){return;// result 被无意义地构造和析构}// ... 执行查询resultQueryResult(mysql_store_result(conn));// ✅ 延后定义if(userId0){return;}MYSQL*conngetConnection();// ... 执行查询QueryResultresult(mysql_store_result(conn));// 需要时才创建}场景3复杂对象的构造#includevector#includemapclassDataProcessor{public:DataProcessor(conststd::vectorintdata):data_(data){// 复杂的预处理preprocess();}// ...private:std::vectorintdata_;std::mapint,intindex_;voidpreprocess(){/* 耗时操作 */}};voidprocessRequest(conststd::vectorintinput,boolneedProcess){// ❌ 不好的做法DataProcessorprocessor(input);if(!needProcess){return;// processor 被无意义地构造包含复杂的预处理}processor.run();// ✅ 好的做法if(!needProcess){return;}DataProcessorprocessor(input);// 确定需要时才构造processor.run();}五、原理深度解析5.1 C 对象生命周期定义点 ──→ 构造函数调用 ──→ 使用期 ──→ 离开作用域 ──→ 析构函数调用延后定义的核心思想就是缩短定义点到使用点之间的距离避免在异常路径或提前返回路径上产生不必要的构造/析构开销。5.2 编译器优化视角现代编译器虽然可以进行一些优化如 RVO/NRVO但对于以下情况优化能力有限带有副作用的构造函数/析构函数涉及动态内存分配的类型虚函数调用classWidget{public:Widget(){std::coutConstruct\n;}Widget(constWidget){std::coutCopy\n;}Widgetoperator(constWidget){std::coutAssign\n;return*this;}~Widget(){std::coutDestruct\n;}};voiddemo(){Widget w;// 输出: ConstructwWidget();// 输出: Construct - Assign - Destruct}// 输出: Destruct// 总共2 次构造、1 次赋值、2 次析构voiddemo2(){Widget wWidget();// 输出: Construct (可能被优化)}// 输出: Destruct// 总共1 次构造、1 次析构5.3 异常安全角度延后定义还能提升异常安全性。考虑以下代码voidriskyFunction(){ResourceA a;// 获取资源AResourceB b;// 获取资源B - 可能抛出异常if(someCondition){throwstd::runtime_error(Error);}ResourceC c;// 获取资源C// ...}如果ResourceB的构造抛出异常ResourceA已经被构造了需要确保它能正确释放。将变量定义延后到真正需要的位置可以减少这种异常安全问题的影响范围。六、注意事项与例外6.1 内置类型无需过度担心对于int、double、char*等内置类型定义成本极低延后定义带来的收益不大// 对于内置类型两种写法差异不大voidfunc(){inti;// 成本极低// ... 很多代码i42;// vs// ... 很多代码inti42;// 稍微清晰一点}6.2 避免过度延后导致代码混乱// ❌ 过度延后代码难以阅读voidbadExample(){// ... 50 行代码{std::string sgetString();process(s);}// s 在这里销毁// ... 又 50 行代码{std::vectorintvgetVector();process(v);}// v 在这里销毁}// ✅ 适度延后在合理的作用域内定义voidgoodExample(){// ... 一些代码std::string sgetString();process(s);// ... 一些代码std::vectorintvgetVector();process(v);}6.3 成员变量无法延后类的成员变量必须在构造函数初始化列表中初始化无法在成员函数中延后定义。对于这种情况可以考虑使用std::optionalC17或指针#includeoptionalclassLazyInit{public:voidinit(){// 延后初始化成员data_.emplace(100);// 真正需要时才构造}private:std::optionalstd::vectorintdata_;// 延后初始化};七、总结要点说明核心原则延后变量定义到真正需要使用的时刻最佳实践定义时直接以初值初始化避免 default 构造后再赋值性能收益避免不必要的构造/析构尤其在异常路径上代码清晰度变量定义靠近使用点代码更易读循环中的选择根据构造析构 vs 赋值的成本权衡请记住尽可能延后变量定义式的出现时间。这样做可增加程序的清晰度并改善程序效率。不只应该延后变量的定义直到非得使用该变量的前一刻为止甚至应该尝试延后这份定义直到能够给它初值实参为止。参考阅读《Effective C》第三版条款26《C Primer》关于变量作用域和生命周期的章节C Core Guidelines: ES.21如果这篇文章对你有帮助欢迎点赞、收藏和转发有任何问题欢迎在评论区留言讨论。