Effective C++ 条款26:尽可能延后变量定义式的出现时间

Effective C++ 条款26:尽可能延后变量定义式的出现时间

只要定义了一个变量而其类型带有一个构造函数或析构函数,那么当程序的控制流到达这个变量定义式时,你便得承受构造成本;当这个变量离开其作用域时,你便得承受析构成本。

一、为什么要延后变量定义?

在 C 语言(特别是 C89)的旧习惯中,开发者往往喜欢在函数开头把所有变量都定义好。这种风格在 C++ 中却可能带来不必要的性能开销。C++ 中的对象往往伴随着构造函数和析构函数的调用,过早定义变量意味着:

  • 不必要的构造开销:变量在定义时就会调用构造函数
  • 不必要的析构开销:即使变量未被使用,离开作用域时也会调用析构函数
  • 代码清晰度下降:读者需要跳过很多行才能看到变量真正被使用的地方

核心原则

不只应该延后变量的定义,直到非得使用该变量的前一刻为止,甚至应该尝试延后这份定义直到能够给它初值实参为止。

二、代码示例对比

反例:过早定义变量

cpp 复制代码
#include <string>
#include <stdexcept>

// 加密密码的函数
std::string encryptPassword(const std::string& password) {
    using namespace std;
    
    // ❌ 过早定义:如果下面抛出异常,这个对象就白构造了
    string encrypted;
    
    if (password.length() < 8) {
        throw logic_error("Password is too short");
        // encrypted 在这里被构造后又立即被析构,完全浪费!
    }
    
    // 真正的加密操作
    const char* key = "MySecretKey";
    encrypted = performEncryption(password, key);
    
    return encrypted;
}

正例:延后到需要使用时才定义

cpp 复制代码
#include <string>
#include <stdexcept>

std::string encryptPassword(const std::string& password) {
    using namespace std;
    
    if (password.length() < 8) {
        throw logic_error("Password is too short");
        // 没有任何对象被无意义地构造和析构
    }
    
    // ✅ 延后定义:此时确定会用到 encrypted
    const char* key = "MySecretKey";
    string encrypted = performEncryption(password, key);
    
    return encrypted;
}

更进一步:直接以初值定义

cpp 复制代码
// ✅✅ 最佳实践:定义时直接初始化
std::string encryptPassword(const std::string& password) {
    if (password.length() < 8) {
        throw std::logic_error("Password is too short");
    }
    
    // 直接以初值定义,避免 default 构造后再赋值
    std::string encrypted(performEncryption(password, "MySecretKey"));
    
    return encrypted;
}

三、性能差异分析

方式 构造函数调用 析构函数调用 赋值操作 效率评级
过早定义 + 后续赋值 1 次 default 1 次 1 次 ⭐⭐
延后定义 + 后续赋值 1 次 default 1 次 1 次 ⭐⭐⭐
延后定义 + 直接初始化 1 次 copy/移动 1 次 0 次 ⭐⭐⭐⭐⭐

对于 std::string 这类带有动态内存分配的类型,default 构造后再赋值的成本远高于直接以初值构造!

循环中的变量定义

cpp 复制代码
// 方式A:变量定义在循环外
Widget w;
for (int i = 0; i < n; ++i) {
    w = 取决于i的某个值;
    // 使用 w
}
// 1 次构造 + 1 次析构 + n 次赋值

// 方式B:变量定义在循环内
for (int i = 0; i < n; ++i) {
    Widget w(取决于i的某个值);
    // 使用 w
}
// n 次构造 + n 次析构

如何选择?

  • 如果赋值成本低于 构造+析构 成本 → 选择方式A
  • 如果构造+析构成本低于赋值成本 → 选择方式B
  • 对于大部分 C++ 类型(尤其是 STL 容器、string 等)→ 通常方式B更优

四、实际应用场景

场景1:文件处理

cpp 复制代码
#include <fstream>
#include <string>

void processFile(const std::string& filepath) {
    // ❌ 不好的做法
    std::ifstream file;
    std::string line;
    std::string content;
    
    if (filepath.empty()) {
        return;  // 三个对象都被无意义地构造和析构了
    }
    
    file.open(filepath);
    // ...
    
    // ✅ 好的做法
    if (filepath.empty()) {
        return;  // 没有任何对象被创建
    }
    
    std::ifstream file(filepath);  // 需要时才创建
    std::string line;
    std::string content;
    // ...
}

场景2:数据库查询

cpp 复制代码
#include <mysql/mysql.h>

class QueryResult {
public:
    QueryResult(MYSQL_RES* res) : result(res) {}
    ~QueryResult() { if (result) mysql_free_result(result); }
    // ...
private:
    MYSQL_RES* result;
};

void fetchUserData(int userId) {
    // ❌ 过早定义
    QueryResult result(nullptr);
    MYSQL* conn = getConnection();
    
    if (userId <= 0) {
        return;  // result 被无意义地构造和析构
    }
    
    // ... 执行查询
    result = QueryResult(mysql_store_result(conn));
    
    // ✅ 延后定义
    if (userId <= 0) {
        return;
    }
    
    MYSQL* conn = getConnection();
    // ... 执行查询
    QueryResult result(mysql_store_result(conn));  // 需要时才创建
}

场景3:复杂对象的构造

cpp 复制代码
#include <vector>
#include <map>

class DataProcessor {
public:
    DataProcessor(const std::vector<int>& data) : data_(data) {
        // 复杂的预处理
        preprocess();
    }
    // ...
private:
    std::vector<int> data_;
    std::map<int, int> index_;
    void preprocess() { /* 耗时操作 */ }
};

void processRequest(const std::vector<int>& input, bool needProcess) {
    // ❌ 不好的做法
    DataProcessor processor(input);
    
    if (!needProcess) {
        return;  // processor 被无意义地构造(包含复杂的预处理!)
    }
    
    processor.run();
    
    // ✅ 好的做法
    if (!needProcess) {
        return;
    }
    
    DataProcessor processor(input);  // 确定需要时才构造
    processor.run();
}

五、原理深度解析

5.1 C++ 对象生命周期

复制代码
定义点 ──→ 构造函数调用 ──→ 使用期 ──→ 离开作用域 ──→ 析构函数调用

延后定义的核心思想就是:缩短"定义点到使用点"之间的距离,避免在异常路径或提前返回路径上产生不必要的构造/析构开销。

5.2 编译器优化视角

现代编译器虽然可以进行一些优化(如 RVO/NRVO),但对于以下情况优化能力有限:

  • 带有副作用的构造函数/析构函数
  • 涉及动态内存分配的类型
  • 虚函数调用
cpp 复制代码
class Widget {
public:
    Widget() { std::cout << "Construct\n"; }
    Widget(const Widget&) { std::cout << "Copy\n"; }
    Widget& operator=(const Widget&) { 
        std::cout << "Assign\n"; 
        return *this; 
    }
    ~Widget() { std::cout << "Destruct\n"; }
};

void demo() {
    Widget w;        // 输出: Construct
    w = Widget();    // 输出: Construct -> Assign -> Destruct
}                   // 输出: Destruct
// 总共:2 次构造、1 次赋值、2 次析构

void demo2() {
    Widget w = Widget();  // 输出: Construct (可能被优化)
}                        // 输出: Destruct
// 总共:1 次构造、1 次析构

5.3 异常安全角度

延后定义还能提升异常安全性。考虑以下代码:

cpp 复制代码
void riskyFunction() {
    ResourceA a;  // 获取资源A
    ResourceB b;  // 获取资源B - 可能抛出异常!
    
    if (someCondition) {
        throw std::runtime_error("Error");
    }
    
    ResourceC c;  // 获取资源C
    // ...
}

如果 ResourceB 的构造抛出异常,ResourceA 已经被构造了,需要确保它能正确释放。将变量定义延后到真正需要的位置,可以减少这种异常安全问题的影响范围。

六、注意事项与例外

6.1 内置类型无需过度担心

对于 intdoublechar* 等内置类型,定义成本极低,延后定义带来的收益不大:

cpp 复制代码
// 对于内置类型,两种写法差异不大
void func() {
    int i;          // 成本极低
    // ... 很多代码
    i = 42;
    
    // vs
    
    // ... 很多代码
    int i = 42;     // 稍微清晰一点
}

6.2 避免过度延后导致代码混乱

cpp 复制代码
// ❌ 过度延后:代码难以阅读
void badExample() {
    // ... 50 行代码
    
    {
        std::string s = getString();
        process(s);
    }  // s 在这里销毁
    
    // ... 又 50 行代码
    
    {
        std::vector<int> v = getVector();
        process(v);
    }  // v 在这里销毁
}

// ✅ 适度延后:在合理的作用域内定义
void goodExample() {
    // ... 一些代码
    
    std::string s = getString();
    process(s);
    
    // ... 一些代码
    
    std::vector<int> v = getVector();
    process(v);
}

6.3 成员变量无法延后

类的成员变量必须在构造函数初始化列表中初始化,无法在成员函数中"延后定义"。对于这种情况,可以考虑使用 std::optional(C++17)或指针:

cpp 复制代码
#include <optional>

class LazyInit {
public:
    void init() {
        // 延后初始化成员
        data_.emplace(100);  // 真正需要时才构造
    }
    
private:
    std::optional<std::vector<int>> data_;  // 延后初始化
};

七、总结

要点 说明
核心原则 延后变量定义到真正需要使用的时刻
最佳实践 定义时直接以初值初始化,避免 default 构造后再赋值
性能收益 避免不必要的构造/析构,尤其在异常路径上
代码清晰度 变量定义靠近使用点,代码更易读
循环中的选择 根据构造+析构 vs 赋值的成本权衡

请记住:

  • 尽可能延后变量定义式的出现时间。这样做可增加程序的清晰度并改善程序效率。
  • 不只应该延后变量的定义,直到非得使用该变量的前一刻为止,甚至应该尝试延后这份定义直到能够给它初值实参为止。

参考阅读:

  • 《Effective C++》第三版,条款26
  • 《C++ Primer》关于变量作用域和生命周期的章节
  • C++ Core Guidelines: ES.21

如果这篇文章对你有帮助,欢迎点赞、收藏和转发!有任何问题欢迎在评论区留言讨论。

相关推荐
加油码1 小时前
位图 BitMap:用一个 bit 管一个状态,空间直接省到位
c++·算法
四代水门1 小时前
LeetCode刷算法题(C++)
c++·算法·leetcode
problc2 小时前
用 JavaScript 打开中国的版式文档:@sharp9/ofdjs 诞生记
开发语言·javascript·ecmascript
devilnumber9 小时前
Java 递归算法 详解 + 核心要点 + 实战运用 + 避坑指南
java·开发语言·算法
kebidaixu10 小时前
BCU 平台 RS485 驱动适配:从 THVD1406 到 ISO3082
linux
unicrom_深圳市由你创科技10 小时前
哪些控制逻辑应该放在 PLC,哪些放在上位机?
c++
asdfg125896311 小时前
JavaBean是什么?怎么理解?有什么用途?
java·开发语言
dsyyyyy110111 小时前
JavaScript变量
开发语言·javascript·ecmascript
玖玥拾12 小时前
C/C++ 基础笔记(十三)继承
c语言·c++·继承