EffctiveC++_01第一章

文章目录

条款01:视 C++ 为一个语言联邦

说明与介绍

核心思想 :C++ 不是一门单一的语言,而是由四个次语言组成的联邦。每个次语言都有自己的规约、习惯用法和高效编程准则。当你从某个次语言切换到另一个时,编程规则也会随之改变。

四个次语言分别是:

  1. C

    • 区块、语句、预处理器、内置数据类型、数组、指针等。
    • 高效编程守则:值传递通常比引用传递更高效(对于内置类型);遵循 C 的命名和作用域规则。
  2. Object-Oriented C++

    • 类、封装、继承、多态、虚函数(动态绑定)等。
    • 高效编程守则:尽量使用 const 引用传递对象;用构造函数初始化列表代替赋值;为多态基类声明虚析构函数。
  3. Template C++

    • 泛型编程、模板元编程(TMP)。
    • 高效编程守则:关注编译期行为;注意模板代码膨胀;使用 typename 指明嵌套从属类型;traits 技术。
  4. STL

    • 容器、迭代器、算法、函数对象等。
    • 高效编程守则:容器和算法紧密配合;了解各种容器的底层实现(如 vector vs list);优先使用算法代替手写循环;函数对象是轻量级的。

启示 :当你编写 C++ 程序时,要清楚自己当前处于哪个次语言中,并采用相应的最佳实践。例如,在 C 部分你可能会用数组和指针,在 STL 部分你会用 vector 和迭代器,在面向对象部分你会用虚函数和多态。


代码示例:展示 C++ 的四个次语言特性

下面这个简单的程序包含了 C、面向对象、模板和 STL 的元素,展示了 C++ 作为语言联邦的多样性。

cpp 复制代码
#include <iostream>
#include <vector>
#include <algorithm>   // for std::sort
using namespace std;

// ========== C 部分:数组与指针 ==========
void cPart() {
    cout << "--- C 部分:数组与指针 ---" << endl;
    int arr[] = {5, 2, 8, 1, 9};
    int size = sizeof(arr) / sizeof(arr[0]);

    // 使用指针遍历数组
    for (int* p = arr; p < arr + size; ++p) {
        cout << *p << ' ';
    }
    cout << endl;
}

// ========== 面向对象部分:类、继承、虚函数 ==========
class Shape {
public:
    virtual void draw() const = 0;   // 纯虚函数,接口继承
    virtual ~Shape() {}               // 虚析构函数(条款07)
};

class Circle : public Shape {
public:
    void draw() const override {
        cout << "绘制圆形" << endl;
    }
};

class Square : public Shape {
public:
    void draw() const override {
        cout << "绘制正方形" << endl;
    }
};

void oopPart() {
    cout << "--- 面向对象部分:多态 ---" << endl;
    Circle c;
    Square s;
    Shape* shapes[] = {&c, &s};
    for (auto shape : shapes) {
        shape->draw();                // 动态绑定
    }
}

// ========== 模板部分:函数模板、类模板 ==========
template<typename T>
T maxValue(T a, T b) {
    return a > b ? a : b;
}

template<typename T, int N>
class FixedArray {
public:
    int size() const { return N; }
    T& operator[](int index) { return data[index]; }
private:
    T data[N];
};

void templatePart() {
    cout << "--- 模板部分:泛型与编译期常量 ---" << endl;
    int a = 10, b = 20;
    cout << "maxValue(10,20) = " << maxValue(a, b) << endl;

    FixedArray<double, 5> arr;
    for (int i = 0; i < arr.size(); ++i) {
        arr[i] = i * 1.1;
        cout << arr[i] << ' ';
    }
    cout << endl;
}

// ========== STL 部分:容器、算法、迭代器 ==========
void stlPart() {
    cout << "--- STL 部分:容器与算法 ---" << endl;
    vector<int> v = {5, 2, 8, 1, 9};
    sort(v.begin(), v.end());          // 使用 STL 算法

    // 使用迭代器遍历
    for (auto it = v.begin(); it != v.end(); ++it) {
        cout << *it << ' ';
    }
    cout << endl;
}

int main() {
    cPart();
    oopPart();
    templatePart();
    stlPart();
    return 0;
}

代码说明

  • C 部分:直接操作数组和指针,体现了 C 风格的底层控制。
  • 面向对象部分:使用类继承、虚函数实现多态,强调动态绑定和接口设计。
  • 模板部分:函数模板和类模板在编译期生成具体代码,展示了泛型编程和编译期计算。
  • STL 部分 :使用 vector 容器、sort 算法和迭代器,体现了数据结构和算法的分离。

这个例子清晰地表明,C++ 程序可以同时运用多种编程范式。开发者需要根据任务选择最合适的范式,并遵循该范式下的高效编程准则------这正是条款01的核心意义。

条款02:尽量以 constenuminline 替换 #define

说明与介绍

核心思想 :这个条款也可以称为"尽量用编译器替换预处理器 "。因为 #define 是预处理指令,它定义的符号在预处理阶段就被直接替换掉了,不会进入编译器的符号表,导致调试困难;同时宏定义可能带来意料之外的副作用。

为什么避免使用 #define

  1. 名称丢失 :宏定义的常量(如 #define ASPECT_RATIO 1.653)在编译错误信息中可能只显示 1.653,而不是 ASPECT_RATIO,增加了调试难度。
  2. 作用域混乱:宏没有作用域限制,一旦定义,在当前文件后续部分都会生效,容易造成命名冲突。
  3. 多次求值:宏函数的参数可能被求值多次,导致性能问题和逻辑错误。
  4. 缺乏类型检查:宏不进行类型检查,可能引发难以发现的错误。

替代方案

  • 常量定义 :用 constconstexpr(C++11)定义常量,具有类型和作用域。
  • 类专属常量 :若常量只在类内使用,用 static const 成员,必要时用 enum 技巧(enum hack)来获得类似宏的"整型常量"特性,且不占用存储空间。
  • 宏函数 :用 inline 模板函数替代,既保证类型安全,又避免多次求值。

代码对比示例

cpp 复制代码
// 文件名: clause02.cpp
// 编译: g++ -std=c++11 clause02.cpp -o clause02

#include <iostream>
using namespace std;

// ========== 错误示例:使用宏定义常量和"函数" ==========
#define ASPECT_RATIO 1.653                    // 无类型,无作用域
#define CALL_WITH_MAX(a, b) ((a) > (b) ? (a) : (b))   // 危险的宏函数

// ========== 正确示例:使用 const 和 inline ==========
const double AspectRatio = 1.653;              // 有类型,进入符号表

template<typename T>
inline T callWithMax(const T& a, const T& b) { // 真正的函数,类型安全
    return a > b ? a : b;
}

// ========== 类专属常量:static const 与 enum hack ==========
class GamePlayer {
public:
    static const int NumTurns = 5;              // 常量声明式(有的编译器需要定义式)
    int scores[NumTurns];                        // 使用常量

    // enum hack:当编译器不支持类内初始化或需要取地址时使用
    enum { NumEnemy = 10 };
    int enemyScores[NumEnemy];
};

// 对于 static const 整数成员,如果在类内已初始化,通常不需要在类外定义;
// 但如果需要取地址,则必须提供定义式(放在实现文件中)。
// const int GamePlayer::NumTurns;  // 定义式(在 .cpp 文件中)

int main() {
    // ===== 1. 常量定义的对比 =====
    cout << "AspectRatio = " << AspectRatio << endl;  // 编译器能识别名称
    // 如果使用宏 ASPECT_RATIO,在错误信息中可能只看到 1.653

    // ===== 2. 宏函数的多次求值问题 =====
    int a = 5, b = 0;
    int max1 = CALL_WITH_MAX(++a, b);          // 危险!宏展开: ((++a) > (b) ? (++a) : (b))
    cout << "宏结果: max = " << max1 << ", a = " << a << endl;
    // 预期: 若 ++a 后 a=6 > 0,返回 ++a(再次递增)=> max=7, a=7
    // 实际输出: max=7, a=7

    // 正确用法:内联模板函数
    a = 5;  // 重新赋值
    int max2 = callWithMax(++a, b);            // ++a 只求值一次
    cout << "内联结果: max = " << max2 << ", a = " << a << endl;
    // 输出: max=6, a=6

    // ===== 3. 类内常量的使用 =====
    GamePlayer gp;
    cout << "GamePlayer::NumTurns = " << GamePlayer::NumTurns << endl;
    cout << "GamePlayer::NumEnemy = " << GamePlayer::NumEnemy << endl;

    return 0;
}

详细解释

1. 常量定义
  • ASPECT_RATIO 只是简单的文本替换,在编译器的符号表中没有这个名字。如果编译出错,错误信息可能提到 1.653 而不是 ASPECT_RATIO,让你摸不着头脑。
  • const 常量 AspectRatio 是一个真正的变量(但通常编译器优化后不分配存储),有类型 double,进入符号表,调试友好。
2. 宏函数的多次求值
  • 宏函数 CALL_WITH_MAX 在调用 CALL_WITH_MAX(++a, b) 时被展开为 ((++a) > (b) ? (++a) : (b))。当 ++a 大于 b 时,++a 会被执行两次:一次在条件中,一次在结果中。这导致结果和副作用都不符合预期。
  • 内联模板函数 callWithMax 是真正的函数,参数只被求值一次(传递的是引用,但 ++a 作为实参只计算一次),行为符合直觉。
3. 类内常量与 enum hack
  • 在类内部定义常量,使用 static const 成员是常见做法。C++98 中,static const 整数成员可以在类内直接初始化,但若需要取地址(如传递指针或引用),则必须在类外提供定义。C++11 后可以使用 constexpr 更灵活。
  • enum hack 是一种旧时代的技巧:利用枚举类型作为整型常量使用。它的特点:
    • 行为更像 #define:不能取枚举成员的地址(因为枚举不是左值),从而避免了某些情况下指向常量的指针导致的链接问题。
    • 不会导致非必要的内存分配(枚举常量不占用存储)。
    • 在模板元编程中,enum 是一种常用技术(旧标准中 static const 整数有时不被视为编译期常量,而枚举一定可以)。
4. 为什么不用 #define 定义宏函数?
  • 宏函数不仅可能多次求值,而且因为没有类型检查,可能传入不兼容的类型导致诡异的结果。内联模板函数则保留了类型安全,同时因为 inline 关键字,短小的函数也可以避免函数调用开销。

运行结果示例

复制代码
AspectRatio = 1.653
宏结果: max = 7, a = 7
内联结果: max = 6, a = 6
GamePlayer::NumTurns = 5
GamePlayer::NumEnemy = 10

从输出可以看到,宏导致 a 被意外递增了两次,而内联函数符合预期。这生动展示了宏的陷阱和替代方案的好处。


总结

  • 宁可用编译器,不用预处理器 。将 #define 替换为 constenuminline,能让代码更安全、更易调试、更符合 C++ 的习惯用法。
  • 对于单纯的常量,最好用 constconstexpr 定义。
  • 对于类内常量,需要取地址时用 static const 并提供定义;不需要取地址时也可用 enum 技巧。
  • 对于形似函数的宏,用 inline 模板函数替代。

条款03:尽可能使用 const

说明与介绍

const 是 C++ 中一个奇妙的关键字,它允许你指定一个语义约束 ------告诉编译器和其他程序员某个值不应该被修改。编译器会强制实施这个约束,如果代码试图修改 const 对象,编译器将报错。

使用 const 的好处:

  • 防止意外修改,让代码更安全、意图更清晰。
  • 允许编译器进行优化 ,例如将 const 对象放入只读内存。
  • 在多线程环境中const 对象通常可以安全共享,无需额外同步。
  • 重载决策 :可以为 const 和 non-const 对象提供不同的函数版本(如 operator[])。

关键概念:

  1. const 与指针
    • const 出现在 * 左边:指针所指的数据是常量(const T*T const*)。
    • const 出现在 * 右边:指针本身是常量,不能指向其他地址(T* const)。
    • 两者同时出现:数据和指针都是常量(const T* const)。
  2. const 成员函数
    • 声明为 const 的成员函数可以被 const 对象调用,承诺不会修改对象的非静态成员(但可以修改 mutable 成员)。
    • 两个成员函数如果只是常量性不同,可以重载。
  3. mutable
    • 即使在 const 成员函数中,也可以修改 mutable 成员,常用于缓存、引用计数等内部状态。
  4. 避免代码重复
    • 当需要同时提供 const 和 non-const 版本时,可以让 non-const 版本调用 const 版本,然后通过 const_cast 去除返回值(或结果)的常量性,从而避免代码重复。

代码对比示例

cpp 复制代码
// 文件名: clause03.cpp
// 编译: g++ -std=c++11 clause03.cpp -o clause03

#include <iostream>
#include <string>
#include <vector>
using namespace std;

// ========== 1. const 与指针的四种组合 ==========
void constPointerExamples() {
    char greeting[] = "Hello";
    const char* p1 = greeting;        // 数据 const,指针可变
    // p1[0] = 'J';                   // 错误!不能修改数据
    p1 = "World";                      // 可以修改指针本身

    char* const p2 = greeting;         // 指针 const,数据可变
    p2[0] = 'J';                        // 可以修改数据
    // p2 = "World";                   // 错误!不能修改指针

    const char* const p3 = greeting;    // 数据和指针都 const
    // p3[0] = 'J';                    // 错误
    // p3 = "World";                   // 错误

    cout << "p1 = " << p1 << endl;      // p1 指向 "World"
    cout << "p2 = " << p2 << endl;      // p2 指向 "Jello" (greeting 被修改)
    cout << "greeting = " << greeting << endl;  // "Jello"
}

// ========== 2. const 成员函数重载 ==========
class TextBlock {
public:
    TextBlock(string s) : text(s) {}

    // const 版本:用于 const 对象
    const char& operator[](size_t pos) const {
        cout << "const 版本被调用" << endl;
        return text[pos];
    }

    // non-const 版本:用于非 const 对象
    char& operator[](size_t pos) {
        cout << "non-const 版本被调用" << endl;
        // 调用 const 版本并去除 const 属性,避免代码重复
        return const_cast<char&>(
            static_cast<const TextBlock&>(*this)[pos]
        );
    }

private:
    string text;
};

// ========== 3. mutable 成员:打破 bitwise constness ==========
class CachedTextBlock {
public:
    CachedTextBlock(string s) : text(s), lengthIsValid(false) {}

    size_t length() const {
        if (!lengthIsValid) {
            textLength = text.length();    // 修改 mutable 成员
            lengthIsValid = true;
            cout << "计算长度" << endl;
        } else {
            cout << "使用缓存长度" << endl;
        }
        return textLength;
    }

private:
    string text;
    mutable size_t textLength;      // 即使在 const 对象中也可修改
    mutable bool lengthIsValid;
};

// ========== 4. 使用 const 的函数参数和返回值 ==========
class Rational {
public:
    Rational(int n = 0, int d = 1) : num(n), den(d) {}
    int numerator() const { return num; }      // const 成员函数
    int denominator() const { return den; }

    // 返回 const 对象,防止 (a*b)=c 这样的赋值
    const Rational operator*(const Rational& rhs) const {
        return Rational(num * rhs.num, den * rhs.den);
    }

private:
    int num, den;
};

// ========== 5. const 可以用于任何作用域的对象 ==========
void constFunction() {
    const int MAX_SIZE = 100;          // 局部 const
    static const double PI = 3.14159;  // 静态 const
    // MAX_SIZE = 200;                 // 错误
}

int main() {
    cout << "=== 1. const 指针示例 ===" << endl;
    constPointerExamples();

    cout << "\n=== 2. const 成员函数重载 ===" << endl;
    TextBlock tb("Hello");
    const TextBlock ctb("World");

    tb[0] = 'h';                       // 调用 non-const 版本
    cout << tb[0] << endl;              // 输出: h

    cout << ctb[0] << endl;             // 调用 const 版本,输出: W
    // ctb[0] = 'w';                    // 编译错误

    cout << "\n=== 3. mutable 成员 ===" << endl;
    const CachedTextBlock cctb("Immutable String");
    cout << "第一次调用 length(): " << cctb.length() << endl;   // 计算
    cout << "第二次调用 length(): " << cctb.length() << endl;   // 使用缓存

    cout << "\n=== 4. const 返回值防止滥用 ===" << endl;
    Rational a(1, 2), b(3, 4);
    Rational c = a * b;                 // 正确
    // (a * b) = c;                      // 如果 operator* 返回 const,这行编译错误,防止无意义赋值

    return 0;
}

运行结果示例

复制代码
=== 1. const 指针示例 ===
p1 = World
p2 = Jello
greeting = Jello

=== 2. const 成员函数重载 ===
non-const 版本被调用
h
const 版本被调用
W

=== 3. mutable 成员 ===
计算长度
第一次调用 length(): 16
使用缓存长度
第二次调用 length(): 16

=== 4. const 返回值防止滥用 ===

关键要点解析

1. const 指针
  • const char* p1:不能通过 p1 修改字符,但 p1 可以指向别处。
  • char* const p2p2 必须始终指向同一个地址,但可以通过它修改内容。
  • const char* const p3:两者都不可变。
2. const 成员函数重载
  • operator[] 的两个版本:const 版本返回 const char&,确保 const 对象不能被修改;non-const 版本返回 char&,允许修改。
  • 代码复用技巧 :non-const 版本通过 static_cast<const TextBlock&>(*this)*this 转为 const 引用,调用 const 版本的 operator[],再用 const_cast 去除返回值的 const 属性。这既保证了逻辑正确,又避免了代码重复。
3. mutable 成员
  • mutable 允许在 const 成员函数中修改特定成员,常用于实现缓存(如本例中的 textLength)、互斥锁、引用计数等。它打破了 bitwise constness (二进制位常量性),但保留了 logical constness(逻辑常量性)。
4. const 返回值
  • operator* 返回 const Rational,防止 (a * b) = c 这样的无意义赋值。如果返回 non-const,这样的表达式可能意外通过编译(虽然极少有人会这样写,但规范的做法是返回 const 值类型)。
5. const 对象
  • 任何类型的对象都可以声明为 const,包括局部变量、全局变量、静态变量。const 对象只能调用其 const 成员函数。

总结

  • 尽可能使用 const :将不改变值的变量、指针、函数参数、成员函数声明为 const
  • const 是编译期强制约束,能帮助你发现错误,提高代码可读性和健壮性。
  • 熟练掌握 const 与指针的组合、const 成员函数重载、mutable 用法以及代码复用技巧,是写出高质量 C++ 代码的关键一步。

条款04:确定对象被使用前已先被初始化

说明与介绍

核心思想 :C++ 对于对象初始化的行为并不总是可预测的,使用未初始化的对象会导致未定义行为。因此,永远在使用对象之前将其初始化

为什么需要关注初始化?

  • C++ 的一部分继承自 C,而 C 对"初始化"并不严格要求(例如局部变量不会被默认初始化)。
  • 对于类对象,构造函数负责初始化,但很容易误用"赋值"代替"初始化",导致效率低下。
  • 不同编译单元(源文件)中定义的非局部静态对象的初始化顺序是未定义的,这会导致难以发现的跨文件依赖问题。

关键规则:

  1. 手动初始化内置类型 :对于内置类型(如 intdouble、指针),因为它们不一定被默认初始化,所以在使用前必须显式初始化。
  2. 使用初始化列表 :在构造函数中,使用成员初始化列表代替构造函数体内的赋值。这样更高效,并且对于某些类型(const 成员、引用成员、没有默认构造函数的类成员)是必须的。
  3. 成员初始化顺序 :成员初始化顺序按照它们在类中声明的顺序进行,而不是初始化列表中的顺序。注意避免使用未初始化的成员去初始化其他成员。
  4. 跨编译单元的初始化顺序问题 :不同源文件中定义的非局部静态对象 (包括全局对象、命名空间作用域对象、类静态对象、文件静态对象)的初始化顺序是不确定的。解决方案是使用局部静态对象 (即在函数内部定义的 static 对象),通过函数返回引用的方式确保在使用前初始化(Meyers 单例模式)。

代码对比示例

cpp 复制代码
// 文件名: clause04.cpp
// 编译: g++ -std=c++11 clause04.cpp -o clause04

#include <iostream>
#include <string>
using namespace std;

// ========== 辅助类,用于观察构造过程 ==========
class PhoneNumber {
public:
    PhoneNumber() {
        cout << "PhoneNumber 默认构造" << endl;
    }
    PhoneNumber(const string& num) : number(num) {
        cout << "PhoneNumber 带参构造: " << number << endl;
    }
    PhoneNumber(const PhoneNumber& rhs) : number(rhs.number) {
        cout << "PhoneNumber 拷贝构造: " << number << endl;
    }
    PhoneNumber& operator=(const PhoneNumber& rhs) {
        cout << "PhoneNumber 赋值运算符: " << rhs.number << endl;
        number = rhs.number;
        return *this;
    }
private:
    string number;
};

// ========== 错误示例:构造函数内使用赋值 ==========
class ABEntryBad {
public:
    // 构造函数体内赋值(不是初始化)
    ABEntryBad(const string& name, const string& phone) {
        cout << "ABEntryBad 构造函数体开始" << endl;
        theName = name;                 // 先调用 string 默认构造,再赋值
        thePhone = PhoneNumber(phone);   // 构造临时对象,然后赋值
        numTimesConsulted = 0;           // 内置类型被赋值(但之前未初始化)
    }
    // 打印函数(简化)
    void print() const {
        // 无法直接打印,仅演示
    }
private:
    string theName;
    PhoneNumber thePhone;
    int numTimesConsulted;
};

// ========== 正确示例:使用初始化列表 ==========
class ABEntryGood {
public:
    // 初始化列表:成员直接构造,效率高
    ABEntryGood(const string& name, const string& phone)
        : theName(name),                 // 直接拷贝构造
          thePhone(phone),                // 直接带参构造
          numTimesConsulted(0)            // 初始化内置类型
    {
        cout << "ABEntryGood 构造函数体开始" << endl;
    }

    // 默认构造函数(也使用初始化列表确保内置类型初始化)
    ABEntryGood()
        : theName(),                       // 调用 string 默认构造
          thePhone(),                      // 调用 PhoneNumber 默认构造
          numTimesConsulted(0)              // 初始化内置类型
    {}

private:
    string theName;
    PhoneNumber thePhone;
    int numTimesConsulted;
};

// ========== 演示成员初始化顺序问题 ==========
class OrderMatters {
public:
    // 注意:初始化列表的顺序是 i(j), j(0),但成员声明顺序是 j 在前,i 在后
    // 实际初始化顺序:先 j(用 0 初始化),然后 i(用 j 初始化,但 j 已为 0)
    OrderMatters(int val) : i(j), j(val) {}   // 危险的写法:i 用未初始化的 j 初始化

    void print() const {
        cout << "i = " << i << ", j = " << j << endl;
    }
private:
    int j;  // 先声明
    int i;  // 后声明
};

// ========== 跨编译单元初始化顺序问题 ==========
// 假设这两个类分别定义在不同的 .cpp 文件中

class FileSystem {  // 来自某个库
public:
    size_t numDisks() const { return 5; }  // 假设有 5 个磁盘
};

// 不好的做法:定义一个全局对象(非局部静态对象)
FileSystem tfs;  // 文件系统的全局对象(在其他文件中可能被使用)

class Directory {
public:
    Directory() {
        // 使用全局 tfs,但 tfs 可能尚未初始化!
        size_t disks = tfs.numDisks();
        cout << "Directory 构造,disks = " << disks << endl;
    }
};

// 全局 Directory 对象
Directory tempDir;  // 依赖于 tfs,但初始化顺序不确定

// ========== 解决方案:使用函数返回局部静态对象(Meyers Singleton) ==========
class FileSystemSafe {
public:
    size_t numDisks() const { return 5; }
};

// 返回局部静态对象的引用,保证在第一次调用时初始化
FileSystemSafe& tfsSafe() {
    static FileSystemSafe fs;  // 局部静态对象,第一次调用时构造
    return fs;
}

class DirectorySafe {
public:
    DirectorySafe() {
        // 通过函数调用获取 FileSystem 对象,确保其已初始化
        size_t disks = tfsSafe().numDisks();
        cout << "DirectorySafe 构造,disks = " << disks << endl;
    }
};

DirectorySafe& tempDirSafe() {
    static DirectorySafe td;
    return td;
}

int main() {
    cout << "=== 错误版本:构造函数内赋值 ===" << endl;
    ABEntryBad bad("Alice", "123456");
    // 输出可能类似于:
    // ABEntryBad 构造函数体开始
    // PhoneNumber 默认构造   (因为 thePhone 先默认构造)
    // PhoneNumber 带参构造: 123456 (临时对象)
    // PhoneNumber 赋值运算符: 123456 (赋值操作)

    cout << "\n=== 正确版本:初始化列表 ===" << endl;
    ABEntryGood good("Bob", "789012");
    // 输出:
    // PhoneNumber 带参构造: 789012   (直接构造,无临时对象)
    // ABEntryGood 构造函数体开始

    cout << "\n=== 成员初始化顺序演示 ===" << endl;
    OrderMatters om(10);
    om.print();  // 输出可能是 i = 随机值(未定义)? 实际 i 用未初始化的 j 初始化,但 j 后初始化为 10,导致 i 未定义
    // 输出: i = 0, j = 10 (在某些编译器上 i 可能为 0,因为 j 在初始化前内存是 0?但这是未定义行为,不可依赖)

    cout << "\n=== 跨编译单元初始化顺序(问题复现) ===" << endl;
    // 这里我们模拟全局对象的问题:如果 tfs 在 tempDir 之后初始化,程序可能崩溃或输出错误
    // 由于我们在同一个文件中定义了两个全局对象,它们的初始化顺序由编译器决定(按定义顺序)
    // 这里 tfs 定义在前,tempDir 在后,所以 tfs 先初始化,看起来正常。
    // 但如果在不同文件中,顺序可能颠倒。

    cout << "\n=== 解决方案:使用局部静态对象 ===" << endl;
    // 通过函数调用获取对象,保证安全
    tempDirSafe();  // 内部调用 tfsSafe(),确保 FileSystemSafe 先初始化

    return 0;
}

运行结果示例(可能因编译器不同略有差异)

复制代码
=== 错误版本:构造函数内赋值 ===
ABEntryBad 构造函数体开始
PhoneNumber 默认构造
PhoneNumber 带参构造: 123456
PhoneNumber 赋值运算符: 123456

=== 正确版本:初始化列表 ===
PhoneNumber 带参构造: 789012
ABEntryGood 构造函数体开始

=== 成员初始化顺序演示 ===
i = 0, j = 10

=== 跨编译单元初始化顺序(问题复现) ===
Directory 构造,disks = 5

=== 解决方案:使用局部静态对象 ===
DirectorySafe 构造,disks = 5

关键要点解析

1. 初始化 vs 赋值
  • 错误版本 ABEntryBad:构造函数体内执行的是赋值操作,不是初始化。成员 theNamethePhone 在进入函数体之前已经通过默认构造函数初始化了,然后在函数体内又被赋值,造成了不必要的开销。numTimesConsulted 虽然是内置类型,但在进入函数体前它的值是未定义的,然后才被赋值。
  • 正确版本 ABEntryGood:使用初始化列表直接在成员构造时传入所需的值,一步到位,高效且安全。对于内置类型,初始化列表也能保证它们被正确初始化(赋初值)。
2. 成员初始化顺序
  • 成员初始化总是按照它们在类中声明的顺序进行,与初始化列表中的顺序无关。
  • OrderMatters 中,j 声明在前,i 在后。因此初始化顺序总是先 ji。初始化列表写的是 i(j), j(val),实际执行顺序是:
    1. jval 初始化(本例 val=10,所以 j=10)。
    2. ij 初始化(此时 j 已为 10,所以 i=10)。
      但实际上由于代码写成了 i(j), j(val)i 试图用尚未初始化的 j 初始化,这是未定义行为。在一些编译器中,未初始化的 j 内存可能为随机值,导致 i 随机。为了避免这种陷阱,应确保初始化列表顺序与声明顺序一致,并且不要用成员去初始化其他成员(除非明确顺序且已初始化)。
3. 内置类型的初始化
  • 在很多上下文中,内置类型(如 intdouble、指针)不会被默认初始化。例如,在函数内部定义的局部变量是未初始化的,它们的值是随机的。因此,必须手工初始化内置类型,无论是直接赋值还是在初始化列表中。
4. 跨编译单元初始化顺序
  • 问题:不同源文件中定义的非局部静态对象 (全局对象、命名空间作用域对象、类的静态成员、文件静态对象)的初始化顺序是未定义的。如果 Directory 的构造函数依赖于 FileSystem 对象,但 FileSystem 对象可能在 Directory 之后初始化,那么 Directory 构造时就会使用未初始化的对象,导致崩溃。
  • 解决方案:将每个非局部静态对象替换为函数内的局部静态对象(Meyers 单例)。函数返回对象的引用,而局部静态对象在第一次调用函数时被初始化。这样,任何代码在使用对象前都会先调用该函数,从而保证对象已初始化。C++11 及以后,这种初始化是线程安全的。

总结

  • 永远在使用对象前初始化它:对于内置类型,手工初始化;对于类类型,确保构造函数初始化所有成员。
  • 使用成员初始化列表,避免在构造函数体内赋值,以提高效率并满足某些类型的初始化要求。
  • 注意成员初始化顺序,按照声明顺序进行,避免用未初始化的成员初始化其他成员。
  • 用局部静态对象替换非局部静态对象,解决跨编译单元的初始化顺序问题。
相关推荐
zhen_hong1 小时前
ReactAgent原理
android·java·javascript
汤姆yu1 小时前
IDEA使用通义灵码做现有项目迭代开发保姆级教程
java·ide·intellij-idea·灵码
我真会写代码1 小时前
Java事务核心原理与实战避坑指南
java·开发语言·数据库
康世行1 小时前
IDEA集成AI辅助工具推荐(好用不卡顿)
java·人工智能·intellij-idea
Zhao_yani1 小时前
微服务核心组件:Gateway
java·微服务·gateway
2401_846341651 小时前
C++动态链接库开发
开发语言·c++·算法
柠檬Leade2 小时前
IDEA中 java: 程序包lombok不存在 问题解决
java·开发语言·maven·intellij-idea·依赖不存在
盐水冰2 小时前
【烘焙坊项目】后端搭建(14) - 工作台&导出数据报表
java·后端·学习
小杍随笔2 小时前
【Rust 语言编程知识与应用:闭包详解】
开发语言·后端·rust