对于C++的初始化列表,一直以来处于知其然而不知其所以然的状态,即知道怎么用,但对于其背后的工作原理却并不了解,这样的情况下,用起来是并不舒服的。仔细研究了一番,发现这一块学问还是比较多的,特做记录。
C++11之前的{}初始化:聚合初始化
在C++11之前,使用{}进行初始化就是可行且常用的,其主要用于两种特定情况:
1.C风格数组的初始化。如:
1 | int numbers[] = {1, 2, 3, 4, 5}; |
2.对“聚合类型”变量的初始化。所谓聚合类型,是指没有用户自定义的构造函数、没有私有 (private) 或保护 (protected) 的非静态成员变量、没有基类、没有虚函数的结构体或类。对于聚合类型,可以使用{}依序初始化其中的成员变量。
1 | struct Point { |
默认初始化与值初始化
以上的两种{}初始化用法中,用得最多的大概就是在定义数组时使用int a[5] = {0};
这样的语法将一个数组的所有元素置为0。这是为什么呢?
C++标准规定,在使用{}进行这样的初始化时,被显式指定的成员会按顺序被初始化为提供的值,而所有未被指定的、排在后面的成员,都会被自动进行“值初始化”。
在发生值初始化时,对于非静态的变量(静态、全局变量会默认被零初始化),
- 对于有构造函数的类类型,值初始化会调用其默认构造函数。
- 对于聚合类型,值初始化会将其所有成员递归地进行值初始化。
- 对于数组类型,值初始化会对数组的每个元素进行值初始化。
- 对于基础数据类型和指针,值初始化会把它们初始化为零。
这就与默认初始化不同。聚合类型、数组类型和基础数据类型的默认初始化并不会将其值初始化为某个特定的值,而仅仅是申请空间。
在定义变量时,int a;
表明a
将被默认初始化,而int a{};
表明a
将被值初始化。
所以,在执行int a[5] = {0};
时,a[0]
会被指定为我们显式定义的0
,而a[1]
~`a[4]则会被值初始化为
0`。
自然地,写成int a[5] = {};
也能实现一样的效果。
C++11引入的std::initializer_list
为了让STL标准库的类和自定义类也能模仿聚合初始化的行为,C++11引入了std::initializer_list
。
std::initializer_list
是一个轻量级的、只读的代理对象。在代码中写下 {a, b, c, d}
这样的初始化列表时:
- 编译器会在内存的某个地方(通常是只读数据段或栈上)创建一个匿名的、临时的常量数组,里面依次存放着
a, b, c, d
的值。 - 然后,编译器会构造一个
std::initializer_list
对象,这个对象内部只包含两个指针(或一个指针和一个长度),一个指向这个临时数组的开头,一个指向结尾。
为一个类编写一个接收 std::initializer_list<T>
作为参数的构造函数,就能够使用初始化列表初始化该类的对象。
那么,MyClass obj{a, b, c}
和MyClass obj = {a, b, c}
这两种写法有何不同呢?C++中,前者称为“直接列表初始化”,后者称为“拷贝列表初始化”,直接列表初始化可以调用explicit构造函数,而拷贝列表初始化不能。
给一个AI生成的例子方便理解:
1 |
|
而在直接列表初始化时,编译器会首先匹配寻找std::initializer_list<T>
构造函数,如果没有对应的构造函数,就会把{}
当成()
,即去匹配普通构造函数。以std::vector
为例:
1 |
|