列表初始化

关于 C++ 中的变量、数组、对象等都有不同的初始化方法,这些繁琐的初始化方法中没有任何一种方式适用于所有的情况。为了统一初始化方式,并且让初始化行为具有确定的效果,在 C++11 中提出了列表初始化的概念。

统一的初始化

在 C++98/03 中,对应普通数组可以直接进行内存拷贝 memcpy() 的对象,可以使用列表初始化来初始化数据。

1
2
3
4
5
6
7
8
9
10
// 数组的初始化
int array[] = { 1,3,5,7,9 };
double array1[3] = { 1.2, 1.3, 1.4 };

// 对象的初始化
struct Person
{
int id;
double salary;
} zhangsan { 1, 3000 };

在 C++11 中,列表初始化变得更加灵活,初始化类对象的示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
using namespace std;

class Test
{
public:
Test(int) {}
private:
Test(const Test &);
};

int main()
{
Test t1(520);
Test t2 = 520;
Test t3 = { 520 };
Test t4 { 520 };
int a1 = { 1314 };
int a2 { 1314 };
int arr1[] = { 1, 2, 3 };
int arr2[] { 1, 2, 3 };

return 0;
}

分析示例代码中使用的各种初始化方式:
(1)t1:通过提供的带参构造进行对象的初始化。
(2)t2:语法错误,因为提供的拷贝构造函数是私有的。
如果拷贝构造函数是公有的,520 会通过隐式类型转换被 Test(int) 构造成一个匿名对象,再通过对这个匿名对象进行拷贝构造得到 t2(这个错误在 VS 中不会出现,在 Linux 中使用 g++ 编译会提示这个错误)
(3)t3 和 t4:使用了 C++11 的初始化方式来初始化对象,效果和 t1 是相同的。
在初始时,{} 前面的等号是否存在对初始化行为没有任何影响。
t3 虽然使用了等号,但是它仍然是列表初始化,因此私有的拷贝构造对它没有任何影响。
(4)arr1、arr2 是基础数据类型的列表初始化方式,和对象的初始化方式是统一的。
(5)a1、a2 的写法,是 C++11 中新添加的语法格式,直接在变量名后加上初始化列表进行变量或者对象的初始化。

使用列表初始化可以对普通类型以及对象进行直接初始化,那么使用 new 操作符创建新对象的时候使用列表初始化进行对象的初始化吗?
答案是肯定的,示例代码如下:

1
2
3
int* p = new int {520};          // 指针 p 指向了一个 new 操作符返回的内存,通过列表初始化将数据初始化为 520
double b = double {52.134}; // 变量 b 是对匿名对象使用列表初始之后,再进行拷贝初始化
int* array = new int[3] {1,2,3}; // 数组 array 在堆上动态分配了一块内存,通过列表初始化的方式直接完成了多个元素的初始化

除此之外,列表初始化还可以用在函数返回值,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
#include <string>
using namespace std;

class Person
{
public:
Person(int id, string name)
{
cout << "id: " << id << ", name: " << name << endl;
}
};

Person func()
{
return { 9527, "华安" };
}

int main()
{
Person p = func();

return 0;
}

代码中的 return { 9527, "华安" }; 相当于 return (9527, "华安" );,直接返回了一个匿名对象。可以看出在 C++11 使用列表初始化是非常方便的,它统一了各种对象的初始化方式,而且代码的书写更加清晰。

列表初始化细节

聚合体

在 C++11 中,列表初始化的使用范围被大大增强了,但是一些模糊的概念也随之而来。列表初始化可以用于自定义类型的初始化,但是对于一个自定义类型,列表初始化可能有两种执行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
#include <string>
using namespace std;

struct T1
{
int x;
int y;
} a = { 123, 321 };

struct T2
{
int x;
int y;
T2(int, int) : x(10), y(20) {}
} b = { 123, 321 };

int main()
{
cout << "a.x: " << a.x << ", a.y: " << a.y << endl;
cout << "b.x: " << b.x << ", b.y: " << b.y << endl;

return 0;
}

示例代码中都是使用列表初始化的方式对对象进行了初始化,但是得到结果却不同。为什么对象 b 并没有被初始化列表中的数据初始化?
对象 a 是对一个自定义的聚合类型进行初始化,它将以拷贝的形式使用初始化列表初始化 T1 结构体中的成员。结构体 T2 中自定义了一个构造函数,因此初始化是通过这个构造函数完成的。如果使用列表初始化对对象初始化,还需要判断这个对象的类型是不是一个聚合体。如果是聚合体,初始化列表中的数据就会拷贝到对象中。

使用列表初始化时,对于什么样的类型 C++ 会认为它是一个聚合体?
(1)普通数组可以看做是一个聚合类型

1
2
3
4
5
6
7
8
int x[] = {1,2,3,4,5,6};
double y[3][3] = {
{1.23, 2.34, 3.45},
{4.56, 5.67, 6.78},
{7.89, 8.91, 9.99},
};
char carry[] = {'a', 'b', 'c', 'd', 'e', 'f'};
std::string sarry[] = {"hello", "world", "nihao", "shijie"};

(2)满足以下条件的类(class、struct、union)可以看做是一个聚合类型
无用户自定义的构造函数、无私有或保护的非静态数据成员、无基类、无虚函数、类中不能使用 {} 和 = 直接初始化的非静态数据成员(C++14 开始支持)

非聚合体

对于聚合类型的类可以直接使用列表初始化进行对象的初始化。如果不满足聚合条件想使用列表初始化其实也可以,需要在类的内部自定义一个构造函数,在构造函数中使用初始化列表对类成员变量进行初始化。

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 <iostream>
#include <string>
using namespace std;

struct T1
{
// 在构造函数中使用初始化列表初始化类成员
T1(int a, double b, int c) : x(a), y(b), z(c) {}
virtual void print()
{
cout << "x: " << x << ", y: " << y << ", z: " << z << endl;
}
private:
int x;
double y;
int z;
};

int main(void)
{
T1 t { 520, 13.14, 1314 }; // ok, 基于构造函数使用初始化列表初始化类成员
t.print();

return 0;
}

另外需要注意的是聚合类型的定义并非递归的,也就是说当一个类的非静态成员是非聚合类型时,这个类也可能是聚合类型,示例代码如下:

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 <iostream>
#include <string>
using namespace std;

struct T1
{
int x;
double y;
private:
int z;
};

struct T2
{
T1 t1;
long x1;
double y1;
};

int main(void)
{
T2 t2 { {}, 520, 13.14 };

return 0;
}

T1 并非一个聚合类型,因为它有一个 Private 的非静态成员。但是 T2 依然是一个聚合类型,可以直接使用列表初始化的方式进行初始化。
t2 对象的初始化过程:对于非聚合类型的成员 t1 初始化,可以直接写一对空的大括号 {},这相当于调用是 T1 的无参构造函数。

std::initializer_list

在 C++ 的 STL 容器中,可以进行任意长度的数据的初始化,使用初始化列表也只能进行固定参数的初始化。如果想要做到和 STL 一样有任意长度初始化的能力,可以使用 std::initializer_list 这个轻量级的类模板来实现。

作为普通函数的参数

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
28
29
30
31
32
33
34
35
#include <iostream>
#include <string>
using namespace std;

void traversal(std::initializer_list<int> a)
{
for (auto it = a.begin(); it != a.end(); ++it)
{
cout << *it << " ";
}
cout << endl;
}

int main()
{
initializer_list<int> list;
cout << "current list size: " << list.size() << endl;
traversal(list);

list = { 1,2,3,4,5,6,7,8,9,0 };
cout << "current list size: " << list.size() << endl;
traversal(list);
cout << endl;

list = { 1,3,5,7,9 };
cout << "current list size: " << list.size() << endl;
traversal(list);
cout << endl;

// 直接通过初始化列表传递数据
traversal({ 2, 4, 6, 8, 0 });
cout << endl;

return 0;
}

std::initializer_list 拥有一个无参构造函数,它可以直接定义实例,此时将得到一个空的 std::initializer_list,在遍历这种类型的容器的时候得到的是一个只读的迭代器,因此不能修改里边的数据,只能通过值覆盖的方式进行容器内部数据的修改。虽然如此,在效率方面也无需担心,std::initializer_list 的效率是非常高的,它的内部不负责保存初始化列表中元素的拷贝,只s存储了初始化列表中元素的引用。

作为构造函数的参数

自定义的类如果在构造对象的时候要接收任意个数的实参,可以给构造函数指定为 std::initializer_list 类型,在自定义类的内部还是使用容器来存储接收的多个实参。

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
28
#include <iostream>
#include <string>
#include <vector>
using namespace std;

class Test
{
public:
Test(std::initializer_list<string> list)
{
for (auto it = list.begin(); it != list.end(); ++it)
{
cout << *it << " ";
m_names.push_back(*it);
}
cout << endl;
}
private:
vector<string> m_names;
};

int main(void)
{
Test t({ "jack", "lucy", "tom" });
Test t1({ "hello", "world", "nihao", "shijie" });

return 0;
}

参考资料

https://subingwen.cn/cpp/list-init/


列表初始化
https://lcf163.github.io/2021/09/16/列表初始化/
作者
乘风的小站
发布于
2021年9月16日
许可协议