1. 什么是成员初始化列表?
它是一种特殊的语法,用在构造函数中,专门用于在对象创建时**"初始化"** 其成员变量。
关键区别:初始化 (Initialization) vs. 赋值 (Assignment)
这是理解初始化列表的核心!
- 初始化:就像婴儿出生时在出生证明上写下名字。这个动作只发生一次,在变量"生命开始"的那一刻。
- 赋值:就像给一个已经存在的人改名字。变量已经存在了,你只是用一个新的值去覆盖它。
成员初始化列表执行的是初始化 ,而在构造函数 {}
函数体内部使用 =
执行的是赋值。
2. 语法和位置
它位于构造函数参数列表的 )
和函数体 {
之间,由一个冒号 :
开始,成员之间用逗号 ,
分隔。
cpp
class MyClass {
int member1;
double member2;
std::string member3;
public:
// 这就是成员初始化列表
MyClass(int a, double b, const std::string& c) : member1(a), member2(b), member3(c) {
// 函数体,现在可以留空,因为初始化都做完了
}
};
:
冒号,表示"接下来是初始化列表"。member1(a)
的意思是:用构造函数的参数a
来初始化成员变量member1
。,
逗号,用于分隔多个成员的初始化。
3. 为什么要用它?(两大好处)
好处一:效率更高
我们来看一个简单的 Box
类的例子,对比两种写法。
写法一:在构造函数体内赋值 (不推荐)
cpp
#include <string>
class Box {
private:
int width;
int height;
std::string name;
public:
// 在函数体内赋值
Box(int w, int h, const std::string& n) {
width = w; // 赋值
height = h; // 赋值
name = n; // 赋值
}
};
编译器的工作流程:
- 进入构造函数前,先为
width
,height
,name
分配内存。 - 对每个成员执行默认初始化 。对于
int
这种内置类型,其值是未定义的(垃圾值)。对于std::string
这种类类型,会调用它的默认构造函数 (创建一个空字符串""
)。 - 进入函数体
{}
。 - 执行
width = w;
,用w
的值覆盖掉width
的垃圾值。 - 执行
height = h;
,用h
的值覆盖掉height
的垃圾值。 - 执行
name = n;
,调用std::string
的赋值运算符,将n
的内容拷贝到name
中,覆盖掉之前创建的空字符串。
这个过程是两步:先默认构造,再赋值。
写法二:使用成员初始化列表 (推荐)
cpp
#include <string>
class Box {
private:
int width;
int height;
std::string name;
public:
// 使用初始化列表
Box(int w, int h, const std::string& n) : width(w), height(h), name(n) {
// 函数体是空的,因为所有工作都做完了
}
};
编译器的工作流程:
- 进入构造函数前,为
width
,height
,name
分配内存。 - 根据初始化列表,直接用
w
来构造width
,用h
来构造height
。 - 对于
name
,直接调用std::string
的拷贝构造函数 ,用n
来构造name
。
这个过程是一步到位:直接用指定的值进行构造。
结论 :对于类类型的成员(如 std::string
),使用初始化列表可以避免一次不必要的默认构造和一次赋值操作,效率更高。对于所有类型,这都是更地道的 C++ 写法。
好处二:某些情况下必须使用
有些类型的成员变量必须在初始化列表中进行初始化,因为它们一旦创建就不能再被赋值。
1. const
(常量) 成员
const
变量必须在声明时或创建时就初始化,之后它的值就不能改变了。
cpp
class Book {
private:
const int ISBN; // 书号是常量,不能更改
public:
// 错误写法:无法编译!
// Book(int num) {
// ISBN = num; // 错误!不能对 const 成员进行赋值
// }
// 正确写法:必须在初始化列表中完成
Book(int num) : ISBN(num) {}
};
2. 引用 (&
) 成员
引用必须在创建时就绑定到一个已存在的对象上,之后不能再改变它引用的对象。
cpp
c
class Student {
private:
std::string& teacherName; // 引用老师的名字
public:
// 错误写法:无法编译!
// Student(std::string& teacher) {
// teacherName = teacher; // 错误!引用必须在创建时初始化
// }
// 正确写法:必须在初始化列表中完成
Student(std::string& teacher) : teacherName(teacher) {}
};
在你的 FunctionRenamer
例子中,Module& wasm_;
就是一个引用成员,所以它必须在成员初始化列表中初始化。
3. 没有默认构造函数的类成员
如果一个成员本身是一个类的对象,而这个类没有提供默认构造函数(即不带参数的构造函数),那么你就必须在初始化列表中明确告诉编译器该如何构造它。
cpp
class Pen {
public:
// 这个类只有一个需要颜色的构造函数,没有默认的
explicit Pen(std::string color) { /* ... */ }
};
class PencilCase {
private:
Pen myPen; // 成员是一个 Pen 对象
public:
// 错误写法:无法编译!
// 编译器不知道如何创建 myPen,因为它没有默认构造函数
// PencilCase(std::string penColor) { /* ... */ }
// 正确写法:必须在初始化列表中告诉编译器如何构造 myPen
PencilCase(std::string penColor) : myPen(penColor) {}
};
一个重要的陷阱:初始化顺序
成员变量的初始化顺序 与它们在初始化列表中的顺序无关 ,只与它们在类中声明的顺序有关!
cpp
class BadExample {
private:
int b; // b 先声明
int a; // a 后声明
public:
// 这是一个有潜在 bug 的写法!
BadExample(int val) : a(val), b(a) {
// 程序员的意图是:先用 val 初始化 a,再用 a 的值初始化 b
}
};
实际执行顺序:
- 因为
b
在类中先被声明,所以先初始化b
。 - 初始化
b
时,它使用了a
的值。但此时a
还没有被初始化,它的值是垃圾值! - 然后才轮到
a
被初始化为val
。
结果:b
的值是未定义的,程序可能随时崩溃。
最佳实践 :为了避免混淆和错误,始终让你的初始化列表的顺序与成员在类中的声明顺序保持一致。
cpp
class GoodExample {
private:
int a;
int b;
public:
// 好的写法:初始化顺序和声明顺序一致
GoodExample(int val) : a(val), b(a) {}
};
总结
-
是什么:构造函数中,用于在对象创建时直接初始化成员变量的特殊语法。
-
为什么用:
- 效率更高:避免了"默认构造 + 赋值"的两步操作。
- 必须使用 :对于
const
成员、引用成员、没有默认构造函数的类成员,这是唯一正确的初始化方式。
-
如何用 :在构造函数参数列表后加冒号
:
,然后是成员(值)
,用逗号分隔。 -
黄金法则:始终让初始化列表的顺序与成员在类中的声明顺序保持一致。