关于 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 }; double b = double {52.134 }; int * array = new int [3 ] {1 ,2 ,3 };
除此之外,列表初始化还可以用在函数返回值,示例代码如下:
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 }; 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/