聚合初始化
从 初始化器列表初始化聚合体。是列表初始化的一种形式。
(aggregate initialization)
。聚合类型可以进行直接列表初始化。没有构造函数也可以初始化其中的参数。但是聚合类型有如下限制
1
2
3
4
5
6
7
8
9
10
11
1. 数组类型 或
2. 满足下列条件的类类型(通常是结构体(struct)或者联合体(union)):
- 没有私有或保护的非静态数据成员
- 没有用户**提供**(user-provided)的显式的或继承的构造函数,还包括:
- 没有默认成员初始化器(没有默认构造函数)
- 没有基类
- 有一说是没有虚函数,保护的或私有的非静态数据成员的基类也可以是聚合类。
- 没有虚函数
- 没有`{}`和`=`直接初始化的非静态数据成员
3. POD类型数据(此处参见深度探索对象模型笔记)**。注意:POD类型是聚合类型的子集。**
1
2
3
4
5
6
7
8
9
10
11
12
13
class test{
public:
struct obj{
int _objval;
};
int _val;
obj inclass_obj;
test(int x, int y): _val(x), inclass_obj._objval(y){} // 错误。这是构造函数初始化列表。用来初始化的。这里的inclass_obj._objval(y)是赋值操作。不允许。详细说就是,初始化列表只能初始化对象。这里是给一个对象的某个变量赋值。这是错误的。
test(int x, int y): _val(x), inclass_obj(y){} // 错误 obj类没有有参构造函数。
test(int x, int y): _val(x), inclass_obj(){} // 可以 obj类有默认无参构造函数。
test(int x, int y): _val(x), inclass_obj{y}{} // 可以 这里的inclass_obj{y}叫做聚合初始化。注意后面的{}是test构造函数的{}。不要搞混
};
针对第一行错误代码,我们可以让obj
自带一个有参构造。然后使用有参构造对obj
进行初始化。
- 这东西真正牛逼的在这呢。如果聚合体中间有嵌套,你可以不用使用花 括号分割
1
2
3
4
5
6
7
struct Aggregate {
int arr[4];
int j;
};
int main(){
Aggregate aggr = {1, 2, 3, 4, 5};
}
列表初始化(花/大括号初始化)
从 花括号初始化器列表 列表初始化对象。
带等号和不带等号一般不做区分。int c{5};
是直接列表初始化 ,int d = {5};
是拷贝列表初始化。 两种形式一般不做区分。
首先第一点:c++不允许在一个类内使用圆括号初始化另一个类。
1
2
3
4
5
6
class test{
int a = 5; //拷贝初始化 OK
int b(5); //直接初始化。ERROR。
int c{5}; //默认类型使用的列表初始化。其实还是调用了对应的构造函数进行了直接初始化。也叫直接列表初始化
int d = {5}; //和上面那种一般不做区分。一般也不用。但是这个叫做拷贝列表初始化。
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class test{
int test_val1;
int test_val2;
public:
test(int x, int y):test_val1(x), test_val2(y){};
struct obj{
int _objval;
int _faf;
obj(){}
obj(int x): _objval(x){} //注意我们obj有自定义构造函数。所以不是聚合体。
};
obj inclass_obj1; //obj对象使用无参构造函数
obj inclass_obj2(); //这是返回obj类型的函数
obj inclass_obj3(5); //这里表面是使用了obj的有参构造,但是不允许这种行为。因为有二义性。编译器认为这是个返回obj类型的形参为5的函数。但是形参为5是啥玩意?所以一个类内不允许使用圆括号初始化另一个类。也就是不允许使用直接初始化
//替代方案:
obj inclass_obj4 = {test_val1}; // 列表初始化(其实还是调用了对应的构造函数) OK //* 注意这里使用了类的有参构造函数。没有使用拷贝构造 //!注意这不是聚合初始化。聚合初始化不能有用户定义的构造函数
obj inclass_obj5{test_val2}; //列表初始化(其实还是调用了对应的构造函数) OK //* 注意这里使用了类的有参构造函数。没有使用拷贝构造 //!注意这不是聚合初始化。聚合初始化不能有用户定义的构造函数
obj inclass_obj6 = 5; //该处的初始化方式是隐式调用obj(int)构造函数生成一个临时的匿名对象,再调用拷贝构造函数完成初始化。
obj inclass_obj7 = obj(5); //显式调用有参构造函数生成一个临时的匿名对象,再调用拷贝构造函数完成初始化 OK
};
为什么使用列表初始化?
可以使用初始化列表接受任意长度。不想细说了,查查就可以。用得少。除了容器类。
所有场合都可用,比如:
- 它能表达一组值,来初始化STL容器:
1 2
std::vector<int> v{1, 3, 5}; std::vector<int> v = {1, 3, 5}; //当然这也可以。
- 它能用来给类的非static成员设定默认值(而
()
就不行)上面提到了。:
1 2 3 4 5 6 7
class Widget { ... private: int x{0}; // 可以 int y = 0; // 可以 int z(0); // 不行 };
- 它和
()
都能用于初始化一个uncopyable的对象(而=
就不行):
1 2 3
std::atomic<int> ai1{0}; // 可以 std::atomic<int> ai2(0); // 可以 std::atomic<int> ai3 = 0; // 不行
- 包括替换
make_pair
1 2 3
unordered_map<int, int> my_map; my_map.insert(make_pair<int, int>(5,10)); //使用make_pair my_map.insert({10,20}); //使用uniform_initialization
使用列表初始化初始内置类型的变量时,防止类型窄化,避免精度丢失的隐式类型转换。
1 2 3
int b = 4.4; //可以。隐式类型转换。 int c = {5.5}; //不可以,列表初始化防止隐式转换带来的窄化。 int d{6.6}; //不可以,列表初始化防止隐式转换带来的窄化。
什么是类型窄化,列表初始化通过禁止下列转换,对隐式转化加以限制:
- 从浮点类型到整数类型的转换
- 从
long double
到double
或float
的转换,以及从double
到float
的转换,除非源是常量表达式且不发生溢出 - 从整数类型到浮点类型的转换,除非源是其值能完全存储于目标类型的常量表达式
- 从整数或无作用域枚举类型到不能表示原类型所有值的整数类型的转换,除非源是其值能完全存储于目标类型的常量表达式
它不会被认为是声明。
- C++中规定“所有看起来像声明的语句都会被视为声明”,这导致
()
在一些场景下会被视为函数声明,而{}
则不会 ```c++ Widget w1(10); // 调用有参构造。 Widget w2(); // 声明了一个无参,名为w2返回Widget的函数 Widget w3{}; // 调用无参构造
- C++中规定“所有看起来像声明的语句都会被视为声明”,这导致
template
1
2
3
4
5
6
7
8
9
10
11
12
13
# 为什么不使用列表初始化?
- 在类有`std::initializer_list`参数的构造函数时,`{}`会有麻烦:`{}`总会被认为是`std::initializer_list`,即使解析出错。
- `auto`会把列表初始化的型别推导为`std::initializer_list`, 值是`{}`内的内容。
- 参见下方`i11`
- 注意区别`i12`
- 一个有趣的地方:如果`{}`中没有元素,那么被调用的是默认构造函数,而不是一个空的`std::initializer_list`。如果你真的想传入一个空的`std::initializer_list`,那么这样:
```c++
Widget w4({});
Widget w5;
std::vector<int>
会有二义性。我们有
1
2
std::vector<int> v1(10, 20); //使用普通构造函数创建vector,里面有10个元素,初始值为20.
std::vector<int> v2{10, 20}; // 使用初始化列表构造函数,创建一个有两个元素的vector,元素分别为10和20
- 同样的情况存在于使用
make
方法进行智能指针的创建之时。make
方法无法使用列表初始化
看一点有意思的事情
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
int i1; //未定义值。undefined value
int i2 = 42; //拷贝初始化 使用42
int i3(42); //直接初始化 使用42
int i4 = int(); //拷贝初始化 使用0(默认值)
int i5{42}; //直接列表初始化 使用42
int i6{}; //直接列表初始化 使用0(默认值)
int i7 = {42}; //拷贝列表初始化 使用42
int i8 = {}; //拷贝列表初始化 使用0 (默认值)
auto i9 = 42; //使用42初始化,推导为int
auto i10{42}; //使用42初始化,推导为int。[除旧编译器外]
auto i11 = {42}; //使用42初始化std::initializer_list<int>。推导为std::initializer_list<int>
auto i12 = int{42}; //使用42初始化的int来初始化i12, 推导为int
int i13(); //这是函数声明
int i14(7,9); //这是编译时错误
int i15 = (7,9); //使用9。这是使用了逗号运算符。
int i16 = int(7,9); //这是编译时错误
auto i17(7,9); //这是编译时错误
auto i18 = (7,9); //使用9,这是使用了逗号运算符。
auto i19 = int(7,9);//这是编译时错误。
//针对不可拷贝和/或不可移动的类型,在C++17以前 如下是不允许的
auto a = std::atomic<int>{9}; //不可拷贝,C++14编译错误。使用了已删除的拷贝构造函数。但是C++17使用了复制省略技术,可以通过编译。
- 逗号运算符
- 整个逗号表达式的值为系列中最后一个表达式的值。
- 从本质上讲,逗号的作用是将一系列运算按顺序执行。
- 视频中建议不要使用
()
进行初始化。尽可能使用花括号{}
i10
的原始语义是使用42初始化一个std::initializer_list<int>
。已在C++14中修复。- 注意
i11
的等号改变了auto
推导的类型。初始化阶段使用等号可能会改变变量的类型。 - 剩余auto解释参见杂记3.
C++17 复制省略技术
参见杂记3
std::initializer_list
参见杂记3
初始化和赋值的区别
当对象在创建时获得了一个特定的值,我们说这个对象被初始化(initialized)。
- 而赋值的含义是把对象的当前值擦除,而以一个新值来替代。
- 初始化不是赋值,初始化的含义是创建变量时赋予其一个初始值
初始化器
看下面的几种初始化之前先看这个
https://zh.cppreference.com/w/cpp/language/initialization
默认初始化 default initialization
1
2
T 对象 //当不带初始化器而声明具有自动、静态或线程局部存储期的变量时;
new T //当以不带初始化器的 new 表达式创建具有动态存储期的对象时
如[new] T object;
这样的都叫做默认(缺省)初始化。比如:
int a
;double b
;bool c
;- …
当我们不使用初始化器的时候,就会自动被编译器调用这种方式初始化。但是它有个问题。在不是类类型的时候,也就是是基本类型的时候,这里的对象值是脏值,也就是不确定值。(有的地方称之为未初始化,但我不确定)
- 如果
T
是类类型,那么考虑各构造函数并实施针对空实参列表的重载决议。调用所选的构造函数(即默认构造函数之一),以提供新对象的初始值;- 说白了就是调用默认构造函数。
- 如果
T
是数组类型,那么该数组的每个元素都被默认初始化; - 否则,不做任何事:具有自动存储期的对象(及其子对象)被初始化为不确定值。
- 这句话翻译过来就是非类类型的参数都是不确定值。也就是脏数据。
- 比如上面的
a
,b
,c
都是不确定值。 - 注意一下,这里说的是自动储存期限。而且下面说了块作用域内。意思就是如果这个基本类型对象是个静态或全局变量,还是会被初始化为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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
int a; //全局
int b;
int c;
void func(){
int d; //块作用域
float e;
double f;
static int n; //块作用域但是静态
static int t;
static int o;
cout << d << endl;
cout << e << endl;
cout << f << endl;
cout << n << endl;
cout << t << endl;
cout << o << endl;
}
int main(){
int x; //块作用域
float y;
double z;
cout << a << endl;
cout << b << endl;
cout << c << endl;
func();
cout << x << endl;
cout << y << endl;
cout << z << endl;
return 0;
}
/*
0 全局 正确零初始化
0
0
848103824 块作用域,脏值
4.59149e-41
6.92062e-310
0 块作用域但是静态,正确零初始化
0
0
848104048 块作用域 脏值
4.59149e-41
0
*/
调用时机:
- 当我们在块作用域内不使用任何初始值定义一个非静态变量时,
- 如果类类型的局部静态变量(块作用域内)没有显式的初始值,它将执行值初始化。非类类型则进行零初始化。
- 当一个类本身含有类类型成员且使用合成的默认构造函数时;
- 当类类型的成员没有在构造函数初始值列表中显式地初始化时;
值初始化 value initialization
那么如果在列表形式中,没有任何参数,也就是如
1
2
3
4
5
6
7
T ();
new T ();
class::class(...) : object() { ... };
T object {};
T {};
new T {};
class::class(...) : object{} { ... };
这样的初始化方法,我们称之为值初始化。
按我的理解,实质应该叫做“广义缺省初始化”。因为值初始化一般是三种处理方式:
- 如果
T
是类类型,且没有默认构造函数,或拥有由用户提供的或被删除的默认构造函数,那么对象是默认初始化。- 直接的默认初始化会导致自动储存期的基本类型成员不会被正确初始化。也就是脏值。
- 如果
T
是类类型,且拥有默认构造函数,而且默认构造函数既不被用户提供,也没有被删除,那么就使用零初始化。然后如果它拥有非平凡的默认构造函数,那么默认初始化它。- 后半句话的意思是,如果类T有默认构造。然后如果有一些数据成员是非静态类型,且这些类型中有的没有平凡的默认构造函数,则当前这个类T的默认构造不是平凡构造。所以说,如果该类T有一个数据成员,比如
string
,则首先类T被零初始化,然后由于类T的默认构造不是平凡的默认构造,所以会再次默认初始化它。也就相当于调用其数据成员的默认构造。(比如string会调用其默认构造初始化为空字符串)。- 由于是先零初始化,再默认初始化。此处保证了自动存储期的基本类型成员可以被零初始化。因为针对自动存储期的基本类型成员,默认初始化不作任何事情。
- 默认构造相当于空初始化器和空函数体。但是依旧会调用各个基类和各个非静态成员的默认构造。
- 后半句话的意思是,如果类T有默认构造。然后如果有一些数据成员是非静态类型,且这些类型中有的没有平凡的默认构造函数,则当前这个类T的默认构造不是平凡构造。所以说,如果该类T有一个数据成员,比如
- 如果
T
是数组类型,那么值初始化数组的每个元素; - 否则,零初始化对象。
以上部分出自官方文档,下面部分来自知乎回答。
- 如果
T
有用户定义的缺省构造函数,直接调用;- 如果
T
有编译器生成的缺省构造函数,先0值初始化再调用;- 如果
T
根本不是类,直接0值初始化。
https://www.zhihu.com/question/36735960/answer/68902926
- 所有情况下,如果使用空花括号对
{}
且T
是聚合类型,那么进行聚合初始化而非值初始化。巨大坑点!!! - 如果
T
是没有默认构造函数但带有接受std::initializer_list
的构造函数的类类型,那么进行列表初始化。
调用时机:
- 在数组初始化的过程中,如果提供的初始值数量少于数组的大小,剩下的元素会进行值初始化;
- 当我们不使用初始值定义一个局部静态变量时;
- 当我们通过书写形如
T()
的表达式显示地请求值初始化时;
加深对象构造和使用初始化器之间的关系理解
所以当我们遇到这样的表达式
1
2
3
T obj;
T obj(...);
int a;
要知道,它并不是调用构造函数的意思。而是通过某种初始化方式得到一个T
类型对象。又恰好由于类型T
是一个带有自定义构造器的类类型,因此对类型T
对象的值初始化等于默认初始化。默认初始化一个类类型,会考虑各构造函数并找到最匹配的那一个,然后调用所选的构造函数,以提供新对象的初始值。所以我们说,这个值初始化过程会包含一次对A
的构造器的调用。并不是说这个表达式就是调用构造器。
零初始化 zero initialization
注意零初始化在语言中没有专用语法,因此下列语法不是零初始化语法。这些是可能会进行零初始化的其他初始化的例子。
1
2
3
4
5
static T 对象
T () ;
T t = {} ;
T {} ; (C++11 起)
CharT 数组 [ n ] = " 短序列 ";
尤其注意非局部静态变量如果不能被常量初始化,那么它会被零初始化。但是类成员变量由于需要类外定义并初始化,所以一开始是零初始化,然后在类外定义的时候根据具体语法规则来进行对应的初始化
将一个对象的初始值设为零。
零初始化的效果是:
如果
T
是标量类型,那么对象的初始值是将整数字面量 0显式转换到T
的值。- 如果
T
是数组类型,那么零初始化每个元素。 - 如果
T
是引用类型,那么不做任何事。 - 如果
T
是非联合体类类型,那么:- 初始化所有填充位为零位
- 零初始化所有非静态数据成员
- 零初始化所有非虚基类子对象
- 如果对象不是基类子对象,那么也零初始化所有虚基类子对象。
- 如果
T
是联合体类型,那么:- 初始化所有填充位为零位
- 零初始化对象的首个非静态具名数据成员。
统一初始化 uniform initialization
个人理解统一初始化的背后其实就是值初始化。目的是让一切看起来更好。
https://blog.csdn.net/danshiming/article/details/116273447
常量初始化
目的是设置静态变量的初值为编译时常量。
如果静态或线程局部变量以常量被初始化,那么就会在其他所有初始化之前进行常量初始化以取代零初始化。
变量或临时对象 obj
在满足以下所有条件时会以常量被初始化 :
要么它有初始化器 (
(表达式列表)
,{初始化器列表}
,=表达式
),要么它的默认初始化会进行某些初始化,并且- 它的初始化完整表达式是常量表达式,或者在
obj
是对象时,该完整表达式也可以为obj
和它的子对象调用constexpr构造函数,即使这些对象不是字面类类型。
- 它的初始化完整表达式是常量表达式,或者在
常量初始化的效果与其所对应的初始化的效果相同,但保证它在任何其他静态或线程局部对象的初始化前完成,并可能在编译时进行。
一般来说,常量初始化在编译时进行,并将预先计算的对象表示作为程序映像的一部分在
.data
段存储。如果变量既为const
又被常量初始化,那么它会被存储于程序映像的只读段.rodata
段。
非局部变量的静态初始化
针对静态变量而言。尤其要注意区分静态成员变量和静态局部变量的区别。单例的笔记中提到了。
所有具有静态存储期的非局部变量的初始化会作为程序启动的一部分在main 函数的执行之前进行(除非被延迟)。所有具有线程局部存储期的非局部变量的初始化会作为线程启动的一部分进行,按顺序早于线程函数的执行开始。对于这两种变量,初始化发生于两个截然不同的阶段:静态初始化和动态初始化。
动态初始化这里先不提。我们关注一下静态初始化:
有两种静态初始化的形式:
- 如果可能,那么应用常量初始化。
- 否则非局部静态及线程局域变量会被零初始化。
实践中:
- 常量初始化通常在编译期进行。预先被计算的对象表示会作为程序映像的一部分存储下来。如果编译器没有这样做,那么它仍然必须保证该初始化发生早于任何动态初始化。
- 零初始化的变量将被置于程序映像的
.bss
段,它不占据磁盘空间,并在加载程序时由操作系统以零填充。
注意,静态成员变量虽然属于具有静态存储期的非局部变量,但是在默认初始化中,格外提到了当不带初始化器而声明具有自动、静态或线程局部存储期的变量时是默认初始化。
个人理解
{}
这花括号本身叫做花括号初始化器列表(Brace-init-list) 在这里
个人理解。当是我们使用花括号初始化器列表进行列表初始化的时候,如果是聚合类型,看见
{}
是聚合初始化(aggregate initialization)如果不是聚合类型
- 如果有
std::initializer_list
构造函数,那么它在能匹配std::initializer_list
构造函数的时候就尽可能使用这个构造函数。 - 如果没有
std::initializer_list
构造函数,那么它叫做统一初始化(value initialization)。他会寻找其他匹配的构造函数参数来寻找合适的方式初始化对象。- 如果是类类型,
- 如果构造函数是编译器合成的,则为零初始化(zero initialization)
- 如果构造函数不是编译器合成的,则是值初始化 (value initialization)
- 如果不是类类型,也就是内置类型,就是零初始化(zero initialization)
- 如果是类类型,
- 如果有
CppCon 2018: Nicolai Josuttis “The Nightmare of Initialization in C++”
https://blog.csdn.net/qq_39583450/article/details/109624599
https://zh.cppreference.com/w/cpp/language/list_initialization
非常诡异的例子
1
2
3
4
5
6
7
8
9
10
struct test{
test() = delete;
int val;
char b;
float c;
double d;
};
int main(){
test obj{};
}
这段代码在C++11/14/17可以运行,但是20不可以。为什么。
首先我们看到test obj{}
的时候可能会想到这是值初始化。但是看好了,test
在C++20前。c++11后是聚合体。所以这是聚合初始化。
聚合体的定义在C++11-C++20间有这样一条:
没有用户提供、继承或 explicit 的构造函数。
首个声明被显式预置或显式弃置的时候,不是由用户提供的
- 所以这里的
=delete
不是用户提供的。所以符合要求
聚合体的定义在C++20后被修改为
没有用户声明或继承的构造函数
=delete
是用户声明的。所以不符合要求了
图片来自这里