1.概念
在我们实际编程过程中,我们经常会碰到变量初始化的问题,不同的初始化方法都有自己适应的范围和作用,最主要的是有没有一种可以通用的初始化方法适应所有的场景,c++11引入了统一的初始化方法,称之为列表初始化。
struct A
{
int a;
int b;
}a = {1, 2};
2.统一的初始化方法
在c++98/03中中我们只能对普通数组和POD(plain old data,简单来说就是可以用memcpy复制的对象)类型可以使用列表初始化,如下:
数组的初始化列表: int arr[3] = {1,2,3}
POD类型如struct A。
但是在c++11将列表初始化适用性放大,可以作为任何类型对象的初始化,如:
class Foo
{
public:
Foo(int) {}
private:
Foo(const Foo &);
};
int main()
{
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};
3.列表初始化过程中需要注意的细节
虽然列表初始化提供了统一的初始化方法,但是同时也会带来一些使用上的疑惑需要各位苦逼码农需要注意,比如对下面的自定义类型的例子:
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来说模式普通的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;
}
// 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) 无基类
4) 无虚函数
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;
}
// 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;
}
// cannot convert from 'initializer-list' to 'Foo'
5) 无{}和=直接初始化的非静态数据成员。
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中包含了私有的非静态数据以及虚函数,用户自定义构造函数,并且使用成员列表初始化方法可以使得非聚合类型的类也可以使用列表初始化方法,因此在这里给各位看官提个建议,在对类的数据成员进行初始化的时候尽量在类的构造函数中用成员初始化列表的方式来对数据成员进行初始化,这样可以防止一些意外的错误。
4.初始化列表
4.1 任何长度的初始化列表
在C++11中,对于任意的STL容易都与和为显示指定长度的数组一样的初始化能力,如:
int arr[] = { 1, 2, 3, 4, 5 };
std::map < int, int > map_t { { 1, 2 }, { 3, 4 }, { 5, 6 }, { 7, 8 } };
std::list<std::string> list_str{ "hello", "world", "china" };
std::vector<double> vec_d { 0.0,0.1,0.2,0.3,0.4,0.5};
STL容易跟数组一样可以填入任何需要的任何长度的同类型的数据,而我们自定义的Foo类型却不具备这种能力,只能按照构造函数的初始化列表顺序进行依次赋值。实际上之所以STL容易拥有这种可以用任意长度的同类型数据进行初始化能力是因为STL中的容器使用了std::initialzer-list这个轻量级的类模板,std::initialzer-list可以接受任意长度的同类型的数据也就是接受可变长参数{...},那么我们是否可以利用这个来改写我们的Foo类,是的Foo类也具有这种能力呢?看下面例子:
struct Foo
{
int x;
int y;
int z;
Foo(std::initializer_list<int> list)
{
auto it= list.begin();
x = *it++;
y = *it++;
z = *it++;
}
};
int _tmain(int argc, _TCHAR* argv[])
{
Foo foo1 {123,456,789};
Foo foo2 { 123, 456};
Foo foo3{ 123};
Foo foo4{ 123, 456, 789,258 };
cout << foo1.x << " " << foo1.y << " " << foo1.z<<endl;
cout << foo2.x << " " << foo2.y << " " << foo2.z << endl;
cout << foo3.x << " " << foo3.y << " " << foo3.z << endl;
cout << foo4.x << " " << foo4.y << " " << foo4.z << endl;
return 0;
}
// 123 456 789
// 123 456 -858993460
// 123 -858993460 -858993460
// 123 456 789
4.2 std::initialzer-list的使用细节
简单了解了initialzer-list后,看看它拥有哪些特点呢?
1、它是一个轻量级的容器类型,内部定义了迭代器iterator等容器必须的一些概念。
2、对于initialzer-list<T>来说,它可以接受任意长度的初始化列表,但是元素必须是要相同的或者可以转换为T类型的。
3、它只有三个成员接口,begin(),end(),size(),其中size()返回initialzer-list的长度。
4、它只能被整体的初始化和赋值,遍历只能通过begin和end迭代器来,遍历取得的数据是可读的,是不能对单个进行修改的。
std::initializer_list<int> list_t ={ 1, 2, 3, 4 };
int _tmain(int argc, _TCHAR* argv[])
{
for (auto it = list_t.begin(); it != list_t.end; it++)
(*it) = 1;
return 0;
}
// 非法
此外initialzer-list<T>保存的是T类型的引用,并不对T类型的数据进行拷贝,因此需要注意变量的生存期。比如我们不能这样使用:
std::initializer_list<int> func(void)
{
auto a = 2, b = 3;
return{ a, b };
}
虽然看起来没有任何问题,且能正常编译通过,但是a,b是在func内定义的局部变量,但程序离开func时变量a,b就销毁了,initialzer-list却保存的是变量的引用,因此返回的将是非法未知的内容。
4.3 防止类型缩窄
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的列表初始化方法定义变量从源头了遏制了类型收窄,使得不恰当的用法就不会用在程序中,避免了某些位置类型的错误,因此建议以后再实际编程中尽可能的使用列表初始化方法定义变量。