统一的初始化方法
在C++98/03中我们只能对普通数组和POD(plain old data,简单来说就是可以用memcpy复制的对象)类型可以使用列表初始化,如下:
数组的初始化列表:
int arr[3] = {1,2,3}
POD类型的初始化列表:
struct A
{
int x;
int y;
}a = {1,2};
在C++11中初始化列表被适用性被放大,可以作用于任何类型对象的初始化。如下:
class Foo
{
public:
Foo(int) {}
private:
Foo(const Foo &);
};
int _tmain(int argc, _TCHAR* argv[])
{
Foo a1(123); //调用Foo(int)构造函数初始化
Foo a2 = 123; //error Foo的拷贝构造函数声明为私有的,该处的初始化方式是隐式调用Foo(int)构造函数生成一个临时的匿名对象,再调用拷贝构造函数完成初始化
Foo a3 = { 123 }; //列表初始化
Foo a4 { 123 }; //列表初始化
int a5 = { 3 };
int a6 { 3 };
return 0;
}
由上面的示例代码可以看出,在C++11中,列表初始化不仅能完成对普通类型的初始化,还能完成对类的列表初始化,需要注意的是a3 a4都是列表初始化,私有的拷贝并不影响它,仅调用类的构造函数而不需要拷贝构造函数,a4,a6的写法是C++98/03所不具备的,是C++11新增的写法。
同时列表初始化方法也适用于用new操作等圆括号进行初始化的地方,如下:
int* a = new int { 3 };
double b = double{ 12.12 };
int * arr = new int[] {1, 2, 3};
让人惊奇的是在C++11中可以使用列表初始化方法对堆中分配的内存的数组进行初始化,而在C++98/03中是不能这样做的。
列表初始化的一些使用细节
虽然列表初始化提供了统一的初始化方法,但是同时也会带来一些使用上的疑惑需要各位苦逼码农需要注意,比如对下面的自定义类型的例子:
struct B
{
int x;
int y;
B(int, int) :x(0), y(0){}
}b = {123,321};
//b.x = 0 b.y = 0
对于自定义的结构体A来说模式普通的POD类型,使用列表初始化并不会引起问题,x,y都被正确的初始化了,但看下结构体B和结构体A的区别在于结构体B定义了一个构造函数,并使用了成员初始化列表来初始化B的两个变量,,因此列表初始化在这里就不起作用了,b采用的是构造函数的方式来完成变量的初始化工作。
那么如何区分一个类(class struct union)是否可以使用列表初始化来完成初始化工作呢?关键问题看这个类是否是一个聚合体(aggregate),首先看下C++中关于类是否是一个聚合体的定义:
-
无用户自定义构造函数。
-
无私有或者受保护的非静态数据成员
-
无基类
-
无虚函数
-
无{}和=直接初始化的非静态数据成员。下面我们逐个对上述进行分析。
1、首先存在用户自定义的构造函数的情况,示例如下:
struct Foo
{
int x;
int y;
Foo(int, int){ cout << "Foo construction"; }
};
int _tmain(int argc, _TCHAR* argv[])
{
Foo foo{ 123, 321 };
cout << foo.x << " " << foo.y;
return 0;
}
输出结果为:
Foo construction -858993460 -858993460
2、类包含有私有的或者受保护的非静态数据成员的情况:
struct Foo
{
int x;
int y;
//Foo(int, int, double){}
protected:
double z;
};
int _tmain(int argc, _TCHAR* argv[])
{
Foo foo{ 123,456,789.0 };
cout << foo.x << " " << foo.y;
return 0;
}
实例中z是一个受保护的成员变量,该程序直接在VS2013下编译出错:
error C2440: 'initializing' : cannot convert from 'initializer-list' to 'Foo'
而如果将z变量声明为static则,可以用列表初始化来,示例:
struct Foo
{
int x;
int y;
//Foo(int, int, double){}
protected:
static double z;
};
int _tmain(int argc, _TCHAR* argv[])
{
Foo foo{ 123,456};
cout << foo.x << " " << foo.y;
return 0;
}
程序输出:123 456,因此可知静态数据成员的初始化是不能通过初始化列表来完成初始化的,它的初始化还是遵循以往的静态成员的额初始化方式。
3、类含有基类或者虚函数:
struct Foo
{
int x;
int y;
virtual void func(){};
};
int _tmain(int argc, _TCHAR* argv[])
{
Foo foo {123,456};
cout << foo.x << " " << foo.y;
return 0;
}
上例中类Foo中包含了一个虚函数,该程序也是非法的,编译不过的,错误信息和上述一样
cannot convert from 'initializer-list' to 'Foo'
struct base{};
struct Foo:base
{
int x;
int y;
};
int _tmain(int argc, _TCHAR* argv[])
{
Foo foo {123,456};
cout << foo.x << " " << foo.y;
return 0;
}
上例中则是有基类的情况,类Foo从base中继承,然后对Foo使用列表初始化,该程序也一样无法通过编译,错误信息仍然为
cannot convert from 'initializer-list' to 'Foo'
4、类中不能有{}或者=直接初始化的费静态数据成员:
struct Foo
{
int x;
int y= 5;
};
int _tmain(int argc, _TCHAR* argv[])
{
Foo foo {123,456};
cout << foo.x << " " << foo.y;
return 0;
}
在结构体Foo中变量y直接用=进行初始化了,因此上述例子也不能使用列表初始化方法,需要注意的是在C++98/03中,类似于变量y这种直接用=进行初始化的方法是不允许的,但是在C++11中放宽了,是可以直接进行初始化的,对于一个类来说如果它的非静态数据成员使用了=或者{}在声明同时进行了初始化,那么它就不再是聚合类型了,不适合使用列表初始化方法了。
在上述4种不再适合使用列表初始化的例子中,需要注意的是一个类声明了自己的构造函数的情形,在这种情况下使用初始化列表是编译器是不会给你报错的,操作系统会给变量一个随机的值,这种问题在代码出BUG后是很难查找到的,因此这种情况不适合使用列表初始化需要特别注意,而其他不适合使用的情况编译器会直接报错,提醒你这些场景下使用列表初始化时不合法的。
那么是否有一种方法可以使得在类不是聚合类型的时候可以使用列表初始化方法呢?相信你肯定猜到了,作为一种很强大的语言不应该也不会存在使用上的限制。自定义构造函数+成员初始化列表的方式解决了上述类是非聚合类型使用列表初始化的限制。看下面的例子:
struct Foo
{
int x;
int y= 5;
virtual void func(){}
private:
int z;
public:
Foo(int i, int j, int k) :x(i), y(j), z(k){ cout << z << endl; }
};
int _tmain(int argc, _TCHAR* argv[])
{
Foo foo {123,456,789};
cout << foo.x << " " << foo.y;
return 0;
}
输出结果为
789 123 456
可见,尽管Foo中包含了私有的非静态数据以及虚函数,用户自定义构造函数,并且使用成员列表初始化方法可以使得非聚合类型的类也可以使用列表初始化方法,因此在这里给各位看官提个建议,在对类的数据成员进行初始化的时候尽量在类的构造函数中用成员初始化列表的方式来对数据成员进行初始化,这样可以防止一些意外的错误。
列表初始化防止类型收窄
C++11的列表初始化还有一个额外的功能就是可以防止类型收窄,也就是C++98/03中的隐式类型转换,将范围大的转换为范围小的表示,在C++98/03中类型收窄并不会编译出错,而在C++11中,使用列表初始化的类型收窄编译将会报错:
int a = 1.1; //OK
int b{ 1.1 }; //error
float f1 = 1e40; //OK
float f2{ 1e40 }; //error
const int x = 1024, y = 1;
char c = x; //OK
char d{ x };//error
char e = y;//error
char f{ y };//error
上面例子看出,用C++98/03的方式类型收窄并不会编译报错,但是将会导致一些隐藏的错误,导致出错的时候很难定位,而利用C++11的列表初始化方法定义变量从源头了遏制了类型收窄,使得不恰当的用法就不会用在程序中,避免了某些位置类型的错误,因此建议以后再实际编程中尽可能的使用列表初始化方法定义变量。