列表初始化
列表初始化
苏丙榅关于C++中的变量,数组,对象等都有不同的初始化方法,在这些繁琐的初始化方法中没有任何一种方式适用于所有的情况。为了统一初始化方式,并且让初始化行为具有确定的效果,在C++11中提出了列表初始化的概念。
1. 统一的初始化
在C++98/03中,对应普通数组和可以直接进行内存拷贝(memcpy())的对象是可以使用列表初始化来初始化数据的
1 | // 数组的初始化 |
在C++11中,列表初始化变得更加灵活了,来看一下下面这段初始化类对象的代码:
1 |
|
具体地来解读一下上面代码中使用的各种初始化方式:
t1
:最中规中矩的初始化方式,通过提供的带参构造进行对象的初始化t2
:语法错误,因为提供的拷贝构造函数是私有的。如果拷贝构造函数是公共的,520
会通过隐式类型转换被Test(int)
构造成一个匿名对象,然后再通过对这个匿名对象进行拷贝构造得到t2
(这个错误在VS中不会出现,在Linux中使用g++编译会提示描述的这个错误,截图如下。)t3
和t4
:使用了C++11的初始化方式来初始化对象,效果和t1
的方式是相同的。- 在初始时,{}前面的等号是否书写对初始化行为没有任何影响。
t3
虽然使用了等号,但是它仍然是列表初始化
,因此私有的拷贝构造对它没有任何影响。
t1、arr1
和t2、arr2
:这两个是基础数据类型的列表初始化方式,可以看到,和对象的初始化方式是统一的。t4
、a2
、arr2
的写法,是C++11中新添加的语法格式,使用这种方式可以直接在变量名后边跟上初始化列表,来进行变量或者对象的初始化。
既然使用列表初始化可以对普通类型以及对象进行直接初始化,那么在使用 new 操作符创建新对象的时候可以使用列表初始化进行对象的初始化吗?答案是肯定的,来看下面的例子:
1 | int * p = new int{520}; |
指针p
指向了一个new操作符返回的内存,通过列表初始化将内存数据初始化为了520变量b
是对匿名对象使用列表初始之后,再进行拷贝初始化。数组array
在堆上动态分配了一块内存,通过列表初始化的方式直接完成了多个元素的初始化。
除此之外,列表初始化还可以直接用在函数返回值上:
1 |
|
代码中的return { 9527, "华安" };
就相当于return (9527, "华安" );
,直接返回了一个匿名对象。通过上面的几个例子可以看出在C++11使用列表初始化是非常便利的,它统一了各种对象的初始化方式,而且还让代码的书写更加简单清晰。
2. 列表初始化细节
2.1 聚合体
在C++11中,列表初始化的使用范围被大大增强了,但是一些模糊的概念也随之而来,在前面的例子可以得知,列表初始化可以用于自定义类型的初始化,但是对于一个自定义类型,列表初始化可能有两种执行结果:
1 |
|
程序执行的结果是这样的:
1 | a.x: 123, a.y: 321 |
在上边的程序中都是用列表初始化的方式对对象进行了初始化,但是得到结果却不同,对象b并没有被初始化列表中的数据初始化,这是为什么呢?
- 对象a是对一个自定义的聚合类型进行初始化,它将以拷贝的形式使用初始化列表中的数据来初始化T1结构体中的成员。
- 在结构体T2中自定义了一个构造函数,因此实际的初始化是通过这个构造函数完成的。
现在很多小伙伴可能就一头雾水了,同样是自定义结构体并且在创建对象的时候都使用了列表初始化来初始化对象,为什么在类内部对对象的初始化方式却不一样呢?因为如果使用列表初始化对对象初始化时,还需要判断这个对象对应的类型是不是一个聚合体,如果是初始化列表中的数据就会拷贝到对象中。
那么,使用列表初始化时,对于什么样的类型C++会认为它是一个聚合体呢?
普通数组本身可以看做是一个聚合类型
1
2
3
4
5
6
7
8int 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"};满足以下条件的类(class、struct、union)可以被看做是一个聚合类型:
无用户自定义的构造函数。
无私有或保护的非静态数据成员。
场景1: 类中有私有成员, 无法使用列表初始化进行初始化
1
2
3
4
5
6
7struct T1
{
int x;
long y;
protected:
int z;
}t{ 1, 100, 2}; // error, 类中有私有成员, 无法使用初始化列表初始化场景2:类中有非静态成员可以通过列表初始化进行初始化,但它不能初始化静态成员变量。
1
2
3
4
5
6
7struct T2
{
int x;
long y;
protected:
static int z;
}t{ 1, 100, 2}; // error结构体中的静态变量 z 不能使用列表初始化进行初始化,它的初始化遵循静态成员的初始化方式。
1
2
3
4
5
6
7
8
9struct T2
{
int x;
long y;
protected:
static int z;
}t{ 1, 100}; // ok
// 静态成员的初始化
int T2::z = 2;
无基类。
无虚函数。
类中不能有使用
{}
和=
直接初始化的非静态数据成员(从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
26
using namespace std;
struct T2
{
int x;
long y;
protected:
static int z;
}t1{ 1, 100 }; // ok
// 静态成员的初始化
int T2::z = 2;
struct T3
{
int x;
double y = 1.34;
int z[3]{1,2,3};
};
int main(void)
{
T3 t{520, 13.14, {6,7,8}}; // error, c++11不支持,从c++14开始就支持了
return 0;
}从C++14开始,使用列表初始化也可以初始化在类中使用{}和=初始化过的非静态数据成员。
2.2 非聚合体
对于聚合类型的类可以直接使用列表初始化进行对象的初始化,如果不满足聚合条件还想使用列表初始化其实也是可以的,需要在类的内部自定义一个构造函数, 在构造函数中使用初始化列表对类成员变量进行初始化:
1 |
|
另外,需要额外注意的是聚合类型的定义并非递归的
,也就是说当一个类的非静态成员是非聚合类型时,这个类也可能是聚合类型
,比如下面的这个例子:
1 |
|
可以看到,T1并非一个聚合类型,因为它有一个Private的非静态成员。但是尽管T2有一个非聚合类型的非静态成员t1,T2依然是一个聚合类型,可以直接使用列表初始化的方式进行初始化。
最后强调一下t2对象的初始化过程,对于非聚合类型的成员t1做初始化的时候,可以直接写一对空的大括号{}
,这相当于调用是T1的无参构造函数。
对于一个聚合类型,使用列表初始化相当于对其中的每个元素分别赋值,而对于非聚合类型,则需要先自定义一个合适的构造函数,此时使用列表初始化将会调用它对应的构造函数。
3. std::initializer_list
在C++的STL容器中,可以进行任意长度的数据的初始化,使用初始化列表也只能进行固定参数的初始化,如果想要做到和STL一样有任意长度初始化的能力,可以使用std::initializer_list
这个轻量级的类模板来实现。
先来介绍一下这个类模板的一些特点:
- 它是一个轻量级的容器类型,内部定义了迭代器
iterator
等容器必须的概念,遍历时得到的迭代器是只读的。
- 对于
std::initializer_list<T>
而言,它可以接收任意长度的初始化列表,但是要求元素必须是同种类型T - 在
std::initializer_list
内部有三个成员接口:size()
,begin()
,end()
。 std::initializer_list
对象只能被整体初始化或者赋值。
3.1 作为普通函数参数
如果想要自定义一个函数并且接收任意个数的参数(变参函数),只需要将函数参数指定为std::initializer_list
,使用初始化列表{ }
作为实参进行数据传递即可。
1 |
|
示例代码输出的结果:
1 | current list size: 0 |
std::initializer_list拥有一个无参构造函数
,因此,它可以直接定义实例,此时将得到一个空的std::initializer_list
,因为在遍历这种类型的容器的时候得到的是一个只读的迭代器,因此我们不能修改里边的数据,只能通过值覆盖的方式进行容器内部数据的修改。虽然如此,在效率方面也无需担心,std::initializer_list的效率是非常高的,它的内部并不负责保存初始化列表中元素的拷贝,仅仅存储了初始化列表中元素的引用。
3.2 作为构造函数参数
自定义的类如果在构造对象的时候想要接收任意个数的实参,可以给构造函数指定为std::initializer_list
类型,在自定义类的内部还是使用容器来存储接收的多个实参。
1 |
|
输出的结果:
1 | jack lucy tom |