在C++11中,初始化列表的使用范围被大大增强了。一些模糊的概念也随之而来。
上一节,读者已经看到了初始化列表可以被用于一个自定义类型的初始化。但是对于一个自定义类型,初始化列表现在可能有两种执行结果:
struct A { int x; int y; } a = { 123, 321 }; // a.x = 123, a.y = 321 struct B { int x; int y; B(int, int) : x(0), y(0) {} } b = { 123, 321 }; // b.x = 0, b.y = 0
其实,上述变量a的初始化过程是C++98/03中就有的聚合类型(Aggregates)的初始化。它将以拷贝的形式,用初始化列表中的值来初始化struct A中的成员。
struct B由于定义了一个自定义的构造函数,因此,实际上初始化是以构造函数进行的。
看到这里,读者可能会希望能够有一个确定的判断方法,能够清晰地知道初始化列表的赋值方式。
具体来说,在使用初始化列表时,对于什么样的类型C++会认为它是一个聚合体?
下面来看看聚合类型的定义:
(1)类型是一个普通数组(如int[10]、char[]、long[2][3])。
(2)类型是一个类(class、struct、union),且
无用户自定义的构造函数。
无私有(Private)或保护(Protected)的非静态数据成员。
无基类。
无虚函数。
不能有{ }和=直接初始化(brace-or-equal-initializer)的非静态数据成员。
对于数组而言,情况是很清晰的。只要该类型是一个普通数组,哪怕数组的元素并非一个聚合类型,这个数组本身也是一个聚合类型:
int x[] = { 1, 3, 5 }; float y[4][3] = { { 1, 3, 5 }, { 2, 4, 6 }, { 3, 5, 7 }, }; char cv[4] = { 'a', 's', 'd', 'f' }; std::string sa[3] = { "123", "321", "312" };
下面重点介绍当类型是一个类时的情况。首先是存在用户自定义构造函数时的例子,代码如下:
struct Foo { int x; double y; int z; Foo(int, int) {} }; Foo foo { 1, 2.5, 1 }; // error
这时无法将Foo看做一个聚合类型,因此,必须以自定义的构造函数来构造对象。
私有(Private)或保护(Protected)的非静态数据成员的情况如下:
struct ST { int x; double y; protected: int z; }; ST s { 1, 2.5, 1 }; // error struct Foo { int x; double y; protected: static int z; }; Foo foo { 1, 2.5 }; // ok
在上面的示例中,ST的初始化是非法的。因为ST的成员z是一个受保护的非静态数据成员。
而Foo的初始化则是成功的,因为它的受保护成员是一个静态数据成员。
这里需要注意,Foo中的静态成员是不能通过实例foo的初始化列表进行初始化的,它的初始化遵循静态成员的初始化方式。
对于有基类和虚函数的情况:
struct ST { int x; double y; virtual void F(){} }; ST s { 1, 2.5 }; // error struct Base {}; struct Foo : public Base { int x; double y; }; Foo foo { 1, 2.5 }; // error
ST和Foo的初始化都会编译失败。因为ST中存在一个虚函数F,而Foo则有一个基类Base。
最后,介绍“不能有{ }和=直接初始化(brace-or-equal-initializer)的非静态数据成员”这条规则,代码如下:
struct ST { int x; double y = 0.0; }; ST s { 1, 2.5 }; // error
在ST中,y在声明时即被=直接初始化为0.0,因此,ST并不是一个聚合类型,不能直接使用初始化列表。
在C++98/03中,对于y这种非静态数据成员,本身就不能在声明时进行这种初始化工作。但是在C++11中放宽了这方面的限制。可以看到,在C++11中,非静态数据成员也可以在声明的同时进行初始化工作(即使用{ }或=进行初始化)。
对于一个类来说,如果它的非静态数据成员在声明的同时进行了初始化,那么它就不再是一个聚合类型,因此,也不能直接使用初始化列表。
对于上述非聚合类型的情形,想要使用初始化列表的方法就是自定义一个构造函数,比如:
struct ST { int x; double y; virtual void F(){} private: int z; public: ST(int i, double j, int k) : x(i), y(j), z(k) {} }; ST s { 1, 2.5, 2 };
需要注意的是,聚合类型的定义并非递归的。简单来说,当一个类的非静态成员是非聚合类型时,这个类也有可能是聚合类型。比如下面这个例子:
struct ST { int x; double y; private: int z; }; ST s { 1, 2.5, 1 }; // error struct Foo { ST st; int x; double y; }; Foo foo { {}, 1, 2.5 }; // OK
可以看到,ST并非一个聚合类型,因为它有一个Private的非静态成员。
但是尽管Foo含有这个非聚合类型的非静态成员st,它仍然是一个聚合类型,可以直接使用初始化列表。
注意到foo的初始化过程,对非聚合类型成员st做初始化的时候,可以直接写一对空的大括号“{ }”,相当于调用ST的无参构造函数。
现在,对于使用初始化列表时的一些细节有了更深刻的了解。对于一个聚合类型,使用初始化列表相当于对其中的每个元素分别赋值;而对于非集合类型,则需要先自定义一个合适的构造函数,此时使用初始化列表将调用它对应的构造函数。