文章目录
-
- [条款01:视 C++ 为一个语言联邦](#条款01:视 C++ 为一个语言联邦)
- 条款02:尽量以 `const`、`enum`、`inline` 替换 `#define`
- [条款03:尽可能使用 `const`](#条款03:尽可能使用
const) - 条款04:确定对象被使用前已先被初始化
-
- 说明与介绍
- 代码对比示例
- 运行结果示例(可能因编译器不同略有差异)
- 关键要点解析
-
- [1. 初始化 vs 赋值](#1. 初始化 vs 赋值)
- [2. 成员初始化顺序](#2. 成员初始化顺序)
- [3. 内置类型的初始化](#3. 内置类型的初始化)
- [4. 跨编译单元初始化顺序](#4. 跨编译单元初始化顺序)
- 总结
条款01:视 C++ 为一个语言联邦
说明与介绍
核心思想 :C++ 不是一门单一的语言,而是由四个次语言组成的联邦。每个次语言都有自己的规约、习惯用法和高效编程准则。当你从某个次语言切换到另一个时,编程规则也会随之改变。
四个次语言分别是:
-
C
- 区块、语句、预处理器、内置数据类型、数组、指针等。
- 高效编程守则:值传递通常比引用传递更高效(对于内置类型);遵循 C 的命名和作用域规则。
-
Object-Oriented C++
- 类、封装、继承、多态、虚函数(动态绑定)等。
- 高效编程守则:尽量使用
const引用传递对象;用构造函数初始化列表代替赋值;为多态基类声明虚析构函数。
-
Template C++
- 泛型编程、模板元编程(TMP)。
- 高效编程守则:关注编译期行为;注意模板代码膨胀;使用
typename指明嵌套从属类型;traits 技术。
-
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:尽量以 const、enum、inline 替换 #define
说明与介绍
核心思想 :这个条款也可以称为"尽量用编译器替换预处理器 "。因为 #define 是预处理指令,它定义的符号在预处理阶段就被直接替换掉了,不会进入编译器的符号表,导致调试困难;同时宏定义可能带来意料之外的副作用。
为什么避免使用 #define?
- 名称丢失 :宏定义的常量(如
#define ASPECT_RATIO 1.653)在编译错误信息中可能只显示1.653,而不是ASPECT_RATIO,增加了调试难度。 - 作用域混乱:宏没有作用域限制,一旦定义,在当前文件后续部分都会生效,容易造成命名冲突。
- 多次求值:宏函数的参数可能被求值多次,导致性能问题和逻辑错误。
- 缺乏类型检查:宏不进行类型检查,可能引发难以发现的错误。
替代方案:
- 常量定义 :用
const或constexpr(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替换为const、enum或inline,能让代码更安全、更易调试、更符合 C++ 的习惯用法。 - 对于单纯的常量,最好用
const或constexpr定义。 - 对于类内常量,需要取地址时用
static const并提供定义;不需要取地址时也可用enum技巧。 - 对于形似函数的宏,用
inline模板函数替代。
条款03:尽可能使用 const
说明与介绍
const 是 C++ 中一个奇妙的关键字,它允许你指定一个语义约束 ------告诉编译器和其他程序员某个值不应该被修改。编译器会强制实施这个约束,如果代码试图修改 const 对象,编译器将报错。
使用 const 的好处:
- 防止意外修改,让代码更安全、意图更清晰。
- 允许编译器进行优化 ,例如将
const对象放入只读内存。 - 在多线程环境中 ,
const对象通常可以安全共享,无需额外同步。 - 重载决策 :可以为
const和 non-const对象提供不同的函数版本(如operator[])。
关键概念:
const与指针 :const出现在*左边:指针所指的数据是常量(const T*或T const*)。const出现在*右边:指针本身是常量,不能指向其他地址(T* const)。- 两者同时出现:数据和指针都是常量(
const T* const)。
const成员函数 :- 声明为
const的成员函数可以被const对象调用,承诺不会修改对象的非静态成员(但可以修改mutable成员)。 - 两个成员函数如果只是常量性不同,可以重载。
- 声明为
mutable:- 即使在
const成员函数中,也可以修改mutable成员,常用于缓存、引用计数等内部状态。
- 即使在
- 避免代码重复 :
- 当需要同时提供
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 p2:p2必须始终指向同一个地址,但可以通过它修改内容。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 对"初始化"并不严格要求(例如局部变量不会被默认初始化)。
- 对于类对象,构造函数负责初始化,但很容易误用"赋值"代替"初始化",导致效率低下。
- 不同编译单元(源文件)中定义的非局部静态对象的初始化顺序是未定义的,这会导致难以发现的跨文件依赖问题。
关键规则:
- 手动初始化内置类型 :对于内置类型(如
int、double、指针),因为它们不一定被默认初始化,所以在使用前必须显式初始化。 - 使用初始化列表 :在构造函数中,使用成员初始化列表代替构造函数体内的赋值。这样更高效,并且对于某些类型(
const成员、引用成员、没有默认构造函数的类成员)是必须的。 - 成员初始化顺序 :成员初始化顺序按照它们在类中声明的顺序进行,而不是初始化列表中的顺序。注意避免使用未初始化的成员去初始化其他成员。
- 跨编译单元的初始化顺序问题 :不同源文件中定义的非局部静态对象 (包括全局对象、命名空间作用域对象、类静态对象、文件静态对象)的初始化顺序是不确定的。解决方案是使用局部静态对象 (即在函数内部定义的
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:构造函数体内执行的是赋值操作,不是初始化。成员theName和thePhone在进入函数体之前已经通过默认构造函数初始化了,然后在函数体内又被赋值,造成了不必要的开销。numTimesConsulted虽然是内置类型,但在进入函数体前它的值是未定义的,然后才被赋值。 - 正确版本
ABEntryGood:使用初始化列表直接在成员构造时传入所需的值,一步到位,高效且安全。对于内置类型,初始化列表也能保证它们被正确初始化(赋初值)。
2. 成员初始化顺序
- 成员初始化总是按照它们在类中声明的顺序进行,与初始化列表中的顺序无关。
- 类
OrderMatters中,j声明在前,i在后。因此初始化顺序总是先j后i。初始化列表写的是i(j), j(val),实际执行顺序是:j用val初始化(本例val=10,所以j=10)。i用j初始化(此时j已为 10,所以i=10)。
但实际上由于代码写成了i(j), j(val),i试图用尚未初始化的j初始化,这是未定义行为。在一些编译器中,未初始化的j内存可能为随机值,导致i随机。为了避免这种陷阱,应确保初始化列表顺序与声明顺序一致,并且不要用成员去初始化其他成员(除非明确顺序且已初始化)。
3. 内置类型的初始化
- 在很多上下文中,内置类型(如
int、double、指针)不会被默认初始化。例如,在函数内部定义的局部变量是未初始化的,它们的值是随机的。因此,必须手工初始化内置类型,无论是直接赋值还是在初始化列表中。
4. 跨编译单元初始化顺序
- 问题:不同源文件中定义的非局部静态对象 (全局对象、命名空间作用域对象、类的静态成员、文件静态对象)的初始化顺序是未定义的。如果
Directory的构造函数依赖于FileSystem对象,但FileSystem对象可能在Directory之后初始化,那么Directory构造时就会使用未初始化的对象,导致崩溃。 - 解决方案:将每个非局部静态对象替换为函数内的局部静态对象(Meyers 单例)。函数返回对象的引用,而局部静态对象在第一次调用函数时被初始化。这样,任何代码在使用对象前都会先调用该函数,从而保证对象已初始化。C++11 及以后,这种初始化是线程安全的。
总结
- 永远在使用对象前初始化它:对于内置类型,手工初始化;对于类类型,确保构造函数初始化所有成员。
- 使用成员初始化列表,避免在构造函数体内赋值,以提高效率并满足某些类型的初始化要求。
- 注意成员初始化顺序,按照声明顺序进行,避免用未初始化的成员初始化其他成员。
- 用局部静态对象替换非局部静态对象,解决跨编译单元的初始化顺序问题。