C++ 列表初始化:{} 到底做了什么?
C++11 引入了统一的列表初始化语法:
cpp
int x{10};
vector<int> v{1, 2, 3};
相比传统的 (),{} 不只是换了一种写法,它还有两个非常重要的特点:
- 可以配合
std::initializer_list - 能够检查并禁止窄化转换
下面只掌握这几个核心概念。
一、{} 可以初始化什么?
最简单的例子:
cpp
int x{10};
int arr[3]{1, 2, 3};
也可以初始化结构体:
cpp
struct MyStruct
{
int a;
char b;
string c;
};
MyStruct s{1, 'a', "hello"};
这里并不是编译器偷偷生成了一个:
cpp
MyStruct(int, char, string);
而是因为 MyStruct 满足聚合类型的条件,所以 {} 可以按照成员顺序直接初始化它:
text
a ← 1
b ← 'a'
c ← "hello"
所以要记住:
聚合初始化不是调用一个"编译器生成的多参数构造函数"。
二、{} 初始化类对象时,会调用构造函数
如果一个类型不是通过聚合初始化,而是通过构造函数初始化,那么 {} 会参与构造函数的匹配。
例如:
cpp
class MyClass
{
public:
MyClass(int x) {}
MyClass(int x, char c) {}
};
那么:
cpp
MyClass a{10};
匹配:
cpp
MyClass(int);
而:
cpp
MyClass b{10, 'a'};
匹配:
cpp
MyClass(int, char);
所以:
{}本身不是构造函数,它只是列表初始化语法;对于类类型,编译器会根据初始化规则寻找合适的构造函数。
三、initializer_list 是 {} 的一个重要用途
看这个例子:
cpp
vector<int> v{1, 2, 3};
这里的:
cpp
{1, 2, 3}
可以用于初始化一个:
cpp
initializer_list<int>
因此 vector 可以通过 initializer_list 构造:
cpp
vector<int> v{1, 2, 3};
最终得到:
text
v = [1, 2, 3]
需要注意:
{1, 2, 3}本身不是一个普通的initializer_list对象,而是一个 braced-init-list(花括号初始化列表)。
当上下文需要时,它可以用于构造 initializer_list。
四、为什么 vector<int> v(10, 20) 和 vector<int> v{10, 20} 不一样?
这是 {} 最经典的坑。
使用 ()
cpp
vector<int> v(10, 20);
这里匹配的是类似:
cpp
vector(size, value);
意思是:
创建 10 个元素,每个元素都是 20。
所以:
text
20 20 20 20 20 20 20 20 20 20
使用 {}
cpp
vector<int> v{10, 20};
如果 vector 有可用的 initializer_list 构造函数,那么列表初始化会优先考虑它。
于是这里表示:
用两个元素 10 和 20 初始化 vector。
结果:
text
10 20
所以一定要记住:
cpp
vector<int> v(10, 20); // 10个20
vector<int> v{10, 20}; // 两个元素:10、20
五、{} 的另一个重要特性:禁止窄化转换
传统初始化:
cpp
int x = 3.14;
通常可以编译,只是会发生:
text
3.14 → 3
但是:
cpp
int x{3.14};
会直接报错。
因为列表初始化会检查窄化转换。
例如:
cpp
int x{3.14}; // ❌
char c{1000}; // ❌
因此可以把 {} 理解成:
初始化的时候更加严格,发现可能丢失信息的转换就拒绝。
这也是为什么现代 C++ 中很多时候推荐:
cpp
int x{10};
而不是:
cpp
int x = 10;
六、explicit 和 {} 的关系
例如:
cpp
class MyClass
{
public:
explicit MyClass(int x) {}
};
因为构造函数是 explicit:
cpp
MyClass a{10}; // ✅
MyClass b(10); // ✅
MyClass c = 10; // ❌
这里可以简单理解成:
MyClass a{10}:直接初始化,可以使用explicit构造函数MyClass b(10):直接初始化,可以使用explicit构造函数MyClass c = 10:需要隐式转换,不能使用explicit构造函数
所以:
explicit主要控制"能不能进行隐式转换",而不是禁止构造函数本身被调用。
七、最后总结
把整篇文章压缩成下面这张表就够了:
| 写法 | 核心含义 |
|---|---|
int x{10}; |
列表初始化 |
int arr[]{1,2,3}; |
初始化数组 |
MyStruct s{1,'a',"hi"}; |
聚合初始化 |
MyClass a{10}; |
根据构造函数进行列表初始化 |
vector<int> v{1,2,3}; |
通常使用 initializer_list |
vector<int> v(10,20); |
构造函数参数为 10,20 |
vector<int> v{10,20}; |
通常表示两个元素 10,20 |
int x{3.14}; |
❌ 禁止窄化转换 |
explicit MyClass(int); |
禁止依靠该构造函数进行隐式转换 |
因此,学习 {} 时真正需要抓住的只有三件事:
① {} 可以进行列表初始化
② {} 会和 initializer_list 产生重要联系
③ {} 会阻止很多窄化转换
这三个点搞明白,C++ 的列表初始化基本就掌握了。