列表初始化是C++11中新增的初始化规则,大大增强了初始化的灵活性,在本篇文章中,我会详细讲解列表初始化的各种使用场景。
1.从C语言初始化语法开始改变——不带赋值号的初始化方式
列表初始化可以说是将C语言的一些初始化语法进行放大的结果。其中不带赋值号的初始化方式是一个很大的改变。
A a初始化就是在C语言中支持的,C++中同样支持。但在C++11中,这种结构体的初始化还能省略赋值=。这是从其它语言上借鉴过来的,在第一时间看到似乎觉得难以置信,但只要习惯了这种“不带赋值号的初始化方式”后,还是很好用的,后续的所有列表初始化都支持“不带赋值号的初始化方式”。
C语言的数组也是如此
注意好好理解不带赋值号的初始化,这对后续的学习很有帮助。
2.内置类型的初始化改变——支持{ }初始化规则
在C语言中内置类型的初始化都是形如int a = 0这样的,但C++11引入了一种新的初始化思想,即“不带赋值号初始化”,所以我们内置类型的初始化方式也有了新的适应方式,即{ }初始化
我们可以理解为,正是因为引入了“不带赋值号初始化”的思想,C++11的内置类型初始化规则才会做出改变。改变后的规则在逻辑上也是严密的。
在struct和数组中通常有多个数据,使用{1, 2, 3}这种方式来初始化。
对于内置类型,只有一个数据,那么我们依然可以用{1}来表达它。这又引入了{ }初始化的思想。从结构体和数组的初始化规则出发向外扩展,使得{ }初始化更加普遍化,统一化。
同时,“不带赋值号初始化”是一种选择,我们也可以像上面结构体和数组的初始化规则那样加上赋值号,这样的话struct、数组、内置类型在初始化上实现了大统一,都可以使用 = { }或者单独的{ },从逻辑上来说这三者在这方面的初始化规则更紧密了。
枚举常量支持这种初始化
联合体也支持这种初始化
3.类对这种初始化规则的适应
很多人都在想struct难道不就是class吗?注意我上面分享的都是C++对C的兼容版本。struct里没有成员函数,只有成员变量。那么对于有成员函数的class,它的初始化规则是怎样的呢?
我们始终要记住“省略赋值号初始化”的和{ }统一初始化的两大目标,类的初始化规则也是根据这两大目标来拓展的。
(1)什么时候识别为C语言的struct?什么时候识别为class?
当有任何和C语言不相关的关键字出现时,都会被识别成class,包括访问限定符,以及class关键字
但要注意struct配上非构造函数的成员函数时,不会报错
这里的识别其实有点细了,稍微留意一下就好了。在绝大多数情况下,我们最好都不要写成C语言的结构体,都尽量写一个C++的类出来,尽量用class,如果用struct也最好写一个构造函数。
(2)类的{ }初始化调用构造函数
对于C++的类来说,依然延续了“省略赋值号初始化”的和{ }统一初始化两大原则
构造函数无参时
构造函数有参时
这里看上去很抽象,但仔细琢磨就会发现初始化的逻辑是合理的,和前面的结构体、内置类型、枚举等初始化规则一致。
4.类的数组的初始化——隐式类型转换的深入理解
这是将类和数组都结合在了一起,我们知道数组和类的统一初始化规则,类的数组同样遵循两大目标,依然保证语法逻辑严密。下面是一个实例,如果前面的理解透彻的话,这里就不会被绕晕
其实这就是套了两层初始化。我以第一个为例,{ { 1 } }应该怎样解读呢?
首先,A a[1]本质上是一个数组,我们就从数组入手,对于数组而言,初始化{ }里面放的是具体初始化的数据(如int a[2]{1, 2}),在这里{ { 1 } }最外层的{ }也是这个意思。
其次,这是一个自定义类型的数组,外层{ }里面放的应该是自定义类型A的具体元素,但是我们应该怎样表示出A的对象呢?这里就需要引入隐式类型转换的概念了。
对于数组而言,要初始化它,外层的{ }里面应该放的是具体的对象,如int a{1, 2, 3},但下面这种初始化同样可以
下面是一种易懂的理解
简单来说,根据列表的类型,编译器生成了相应个数的变量,这些变量用逗号之间的表达式初始化,最后再将这些变量存到内存中。因此,我们所说的隐式类型转换,其实可以理解为对自动生成的相应类型的变量进行初始化,最后将这些自动生成的变量存到内存中。如果说还有缺失的值,如int[10]只主动初始化4个,那么剩余的元素会用匿名对象的值填充。
我们可以用这样的思路解释刚刚的两种写法,而且应该会很轻松了。
对于所有自定义类型的列表都是这样,除了数组,类也是如此
但是一定要注意,只有自定义类型的列表才会自动生成这个变量,如int arr[],甚至int arr[1]也会,但是内置类型的就不会,列表里的内容不会进行任何转换,如int a,否则逻辑上会发生无穷递归的情况(和拷贝构造的引用类似,会无限生成变量)。
对于自定义类型来说一定会生成一个变量来接收列表中的内容用于变量初始化,也就是隐式类型转换。
深刻理解隐式类型转换是掌握C++11列表初始化的精髓,一定要用适合自己的方法理解这部分。
5.new中的列表初始化——沿用定义时初始化的思路,加深理解隐式类型转换的场景,指针转换
在内置类型中,初始化可用传统的( )来进行,也可以用统一的{ }来进行。这也是“无赋值号初始化”思想引入初始化的一个巧合之处,本来new后的初始化就是紧跟着的,现在有一个统一的支持“无赋值号初始化”的{ }初始化方式,刚好可以完美替代原来的写法。
注意隐式类型转换依然只存在于new自定义类型,按我刚才分享的自动生成变量的思路来讲,内置类型是不会生成变量的,列表中必须是严格的匹配值(包括指针也算内置类型)
也不能发生任何指针类型的隐式类型转换
在这里顺便补充一下,C++中指针都必须手动强转,不仅是上面的那张图,所有场景都是如此,这是C++不同于C语言的一处细节
其余用法没有区别,注意的事项和上面提到的都一样
注意缺失值都是按匿名对象的值填充,int()一定是0,在所有编译器上都是,而不是靠编译器选择
6.用变量初始化
用变量初始化也同样遵循上面的原则,只不过需要注意对于内置类型,不会进行任何的隐式类型转换(没有变量生成),就算是整型提升或截断也不行
在要生成变量的数组或类中才能不主动强转(中间生成的变量会隐式类型转换一次),但也会有警告,所以最好避免这些情况
7.initializer_list
initializer_list本质上也是“不带赋值符号初始化”和“{ }统一初始化”的产物,其实我上面讲到的所有单参数列表中{ }都可以叫做初始化列表,如int a{ 1 }就算。多参数列表里面的{ }按照自动生成变量的理解方式来看也可以叫做初始化列表,这一点其实很灵活,因为初始化列表本身就是一个很灵活的容器,只要是{ }格式的都可以算作初始化列表
initializer_list可以接收任意数量的元素,即{ }里面可以随便放,这也是为了兼容数组初始化时int a[] = { }里面可以放任意个数据。接收到的initializer_list会从最小元素开始去自动匹配对应的构造函数(如类的数组{ {1}, {1, 2} }会先把{1}, {1, 2}拿去调用类的构造,生成对象),内置类型也会调用对应的构造(匿名构造的统一,如int(1)),最后如果最外层{ }有数组就分别存储它们的值。
在C++11后,所有容器都支持用initializer_list来初始化
因为如果不显式支持initializer_list初始化的话,当使用{ }这种统一方式来调用构造函数的话,传的参数个数不同可能会导致匹配到不同的构造函数,如果专门有个initializer_list的构造函数的话,就不会乱匹配,导致功能错误
有initializer_list构造函数
无initializer_list构造函数
如果只保留一个,那当然也能匹配
仔细理解initializer_list,它本质上就是一种为了适应新语法而诞生的一种容器,它的出现使得之前数组的初始化语法(int arr[] = { })更有逻辑性,它匹配构造函数的原则也解释了上面列表初始化的更深的原理。
列表初始化涉及的点特别地多,单看某一部分的语法可能还行,但要串联起这个逻辑却是很难的,因为它是一种旧语法的延伸,将原本简单的数组初始化语法扩展为庞大的统一化的初始化体系,还引入了其它语法的思想。这需要我们慢慢理解背后的逻辑,消化好之后会发现列表初始化其实挺好用的。