0%

再看C++初始化列表

对于C++的初始化列表,一直以来处于知其然而不知其所以然的状态,即知道怎么用,但对于其背后的工作原理却并不了解,这样的情况下,用起来是并不舒服的。仔细研究了一番,发现这一块学问还是比较多的,特做记录。

C++11之前的{}初始化:聚合初始化

在C++11之前,使用{}进行初始化就是可行且常用的,其主要用于两种特定情况:

1.C风格数组的初始化。如:

1
2
int numbers[] = {1, 2, 3, 4, 5};
char message[] = "hello"; // 这其实是 {'h','e','l','l','o','\0'} 的简写

2.对“聚合类型”变量的初始化。所谓聚合类型,是指没有用户自定义的构造函数、没有私有 (private) 或保护 (protected) 的非静态成员变量、没有基类、没有虚函数的结构体或类。对于聚合类型,可以使用{}依序初始化其中的成员变量。

1
2
3
4
5
struct Point {
int x;
int y;
};
Point p = {10, 20};

默认初始化与值初始化

以上的两种{}初始化用法中,用得最多的大概就是在定义数组时使用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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <vector>

struct MyVector {
// 声明一个 explicit 的构造函数
explicit MyVector(int size) {
std::cout << "Explicit constructor called with size: " << size << std::endl;
}

// 声明一个接收 std::initializer_list 的构造函数
MyVector(std::initializer_list<int> list) {
std::cout << "Initializer_list constructor called." << std::endl;
}
};

int main() {
// --- 直接列表初始化 ---
MyVector v1 {10}; // OK! 直接初始化可以调用 explicit 构造函数

// --- 拷贝列表初始化 ---
MyVector v2 = {10}; // 编译错误! 拷贝初始化不能调用 explicit 构造函数

// 另一个例子:
MyVector v3 {1, 2, 3}; // OK! 调用 initializer_list 构造函数
MyVector v4 = {1, 2, 3}; // OK! 调用 initializer_list 构造函数
}

而在直接列表初始化时,编译器会首先匹配寻找std::initializer_list<T>构造函数,如果没有对应的构造函数,就会把{}当成(),即去匹配普通构造函数。以std::vector为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>
#include <vector>

int main() {
// 场景 A: 使用花括号 {} 进行列表初始化
std::vector<int> v1{10, 20};

// 编译器的思考过程:
// 1. 看到 {...},优先寻找 std::initializer_list<int> 构造函数。
// 2. std::vector 有一个 `vector(std::initializer_list<int>)` 构造函数。
// 3. 参数 {10, 20} 能完美匹配。
// 4. 锁定并调用此构造函数。
// 结果:v1 是一个包含两个元素(10 和 20)的 vector。
std::cout << "v1.size() = " << v1.size() << std::endl; // 输出 2


// 场景 B: 使用圆括号 () 进行常规构造
std::vector<int> v2(10, 20);

// 编译器的思考过程:
// 1. 看到 (...),进行常规的重载决策。
// 2. 寻找能匹配 (int, int) 的构造函数。
// 3. 找到了 `vector(size_type count, const T& value)` 这个构造函数。
// 4. 锁定并调用此构造函数。
// 结果:v2 是一个包含十个元素(每个都是 20)的 vector。
std::cout << "v2.size() = " << v2.size() << std::endl; // 输出 10
}