C++类初始化方法
C++ 提供了多种初始化类对象的方式,不同方式的语义和行为有微妙差异。本文逐一介绍。
1. 默认初始化
当不提供任何初始值时发生:
1 | class Foo { |
注意:内置类型(int、double、指针等)在默认初始化时值是未定义的,读取属于未定义行为。
2. 值初始化
使用空括号或 {} 触发,内置类型会被零初始化:
1 | Foo f1(); // 注意:这是函数声明,不是对象! |
注意:Foo f1() 是最令人困惑的解析(Most Vexing Parse),编译器将其视为函数声明。用 {} 可以避免这个问题。
3. 直接初始化
在括号中提供参数,直接调用匹配的构造函数:
1 | class Foo { |
4. 拷贝初始化
使用 = 触发,先构造临时对象再拷贝(编译器可能优化掉拷贝步骤):
1 | Foo f1 = 42; // 拷贝初始化 |
拷贝初始化要求对应的构造函数不是 explicit 的,否则编译报错:
1 | class Bar { |
5. 列表初始化(C++11)
使用花括号 {} 进行初始化,C++11 引入。有两个重要特点:
5.1 禁止窄化转换
1 | class Foo { |
5.2 优先匹配 initializer_list 构造函数
1 | class Bar { |
这一点是 vector 等容器容易踩坑的地方:
1 | std::vector<int> v1(10, 1); // 10 个元素,每个值为 1 |
6. new 动态初始化
使用 new 在堆上创建对象时,同样遵循上述初始化规则:
1 | Foo* p1 = new Foo; // 默认初始化,p1->x 未定义 |
new[] 创建数组时:
1 | Foo* arr1 = new Foo[3]; // 每个元素默认初始化 |
注意:动态分配的对象必须手动 delete 释放,建议优先使用智能指针:
1 | auto sp1 = std::make_shared<Foo>(); // 值初始化 |
7. 聚合初始化
对于聚合类型(无自定义构造函数、无私有/保护成员、无基类、无虚函数),可以直接用 {} 初始化成员:
1 | struct Point { |
C++11 起也支持对含默认成员初始值的聚合类型初始化:
1 | struct Config { |
8. 构造函数中的成员初始化列表
成员初始化列表是构造函数中初始化成员的首选方式,比在构造函数体内赋值更高效:
1 | class Person { |
以下情况必须使用成员初始化列表:
const成员- 引用成员
- 没有默认构造函数的类类型成员
1 | class Wrapper { |
成员的初始化顺序取决于声明顺序,而非初始化列表中的书写顺序。写错顺序可能导致未定义行为:
1 | class Bad { |
9. C++11 类内默认初始值
可以在类定义中直接为成员提供默认值:
1 | class Settings { |
这比在多个构造函数的初始化列表中重复写默认值更简洁,也避免了遗漏。
10. 小结
| 方式 | 语法 | 关键点 |
|---|---|---|
| 默认初始化 | Foo f; |
内置类型值未定义 |
| 值初始化 | Foo f{}; |
内置类型零初始化 |
| 直接初始化 | Foo f(42); |
直接调用构造函数 |
| 拷贝初始化 | Foo f = 42; |
受 explicit 限制 |
| 列表初始化 | Foo f{42}; |
禁止窄化,优先匹配 initializer_list |
| new 动态初始化 | new Foo(42); |
堆上分配,遵循相同初始化规则 |
| 聚合初始化 | Point p{1,2}; |
适用于聚合类型 |
| 成员初始化列表 | : name(n) |
构造函数中首选方式 |
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 iehtian!
