()和{}初始化的用法

  • 大括号初始化可以应用的语境最为宽泛。可以避免令人苦恼的解析语法、可以阻止隐式窄化型别转换
  • 构造函数重载决议期间,只要有任何可能,大括号初始化物就会与带有std::initializer_list型别的形参想匹配,即使其他重载版本更合适
  • 使用两个实参来创建std::vector<数值类型>结果会大相径庭。这是大括号与小括号之间的一个明显不同的例子

我们指定初始化的方式包括使用小括号、使用等号或者使用大括号。

int x(0);	//使用小括号初始化
int y = 0;	//使用等号初始化
int z{
     0};	//使用大括号初始化

int z = {
     0};	//等号加大括号的用法等同于使用大括号

很多人喜欢使用等号来书写初始化语句,新手往往会认为这里面会发生一次赋值,但是实际上是没有的。我们使用int等内建型别不需要区分的这么开,但是当我们使用自定义型别的时候,就必须区分开初始化和赋值的概念了。

Widget w1;		//调用的是默认构造函数

Widget w2 = w1;	//是初始化而并非赋值,调用的是复制构造函数

w1 = w2;		//并非赋值,调用的是复制赋值运算符

C++11中引入了统一初始化:单一的、至少从概念上可以用于一切场合、表达一切意思的初始化。它的基础是大括号形式。

大括号初始化可以表达之前无法表达之事。

使用大括号来指定容器的初始内容非常简单:

std::vector<int> v{
     1,3,5};  //v的初始内容为1,3,5

大括号初始化可以为非静态成员指定默认初始化值,C++11中也可以使用“=”初始化语法,但是不能使用小括号:

class Widget{
     
private:
	int x{
     0};  
	int y = 0;
	//int z(0);		//错误
};

不可复制的对象可以采用大括号和小括号来初始化,但是不能使用“=”

std::atomic<int> ai1{
     0};
std::atomic<int> ai2(0);
//std::atomic ai3 = 0;    //错误

通过上述情况我们可以看到,这三种初始化方法中,只有大括号初始化方法适用所有场合。

不过大括号初始化有一项新特性,就是它禁止内建型别之间进行隐式窄化型别转换。如果大括号内的表达式无法保证能够采用进行初始化的对象来表达,则代码不能通过编译,不过小括号和等号可以:

double x,y,z;

//int sum{x + y + z};			//错误,double型别之和可能无法通过int表达
int sum2(x + y + z);
int sum3 = x+y+z;

大括号初始化的一项特征是,它对于C++的最令人苦恼的解析语法免疫。C++规定:任何能够解析为声明的都要解析为声明,而这会带来副作用。程序员本来想要以默认方式构造一个对象,结果却不小心声明了一个函数。这个错误的根本原因在于构造函数调用语法。

//以传递参数方式调用构造函数
Widget w1(10);		//调用Widget的构造函数,传入形参10

//如果调用一个没有形参的Widget构造函数的话,结果却变成了声明一个函数而非对象
Widget w2();		//声明了一个名为w2,返回一个Widget型别对象的函数

由于函数声明不能使用大括号来指定形参列表,所以使用大括号来完成对象的默认构造没有这个问题:

Widget w3{
     };		//调用没有形参的构造函数

大括号初始化存在一些缺陷,伴随大括号初始化有时会出现意外行为。这种行为源于大括号初始化物、std::initializer_list以及构造函数重载决议之间的纠结关系。这几者之间的相互作用可以使代码看起来要做某一件事,实际上却在做另一件事。比如使用大括号初始化物来初始化一个使用auto声明的变量,那么推导出来的型别就会变成std::initializer_list。

在构造函数被调用时,只要形参中没有任何一个具备std::initializer_list型别,那么小括号和大括号的意义就没有区别。如果一个或多个构造函数声明了任何一个具备std::initializer_list型别的形参,那么采用大括号初始化语法的调用语句会优先选用带有std::initializer_list型别形参的重载版本。即使是平常会执行复制或移动的构造函数也会被带有std::initializer_list型别形参的构造函数劫持:

//构造函数没有std::initializer_list型别,大括号和小括号初始化没有区别
class Widget{
     
public:
	Widget(int i,bool b);
	Widget(int i,double d);
};

Widget w1(10,true);		//调用第一个构造函数
Widget w2{
     10,true};		//调用第一个构造函数

Widget w3(10,5.0);		//调用第二个构造函数
Widget w4{
     10,5.0};		//调用第二个构造函数
//构造函数一旦有std::initializer_list形参,那么大括号初始化一定会选用这个构造函数
class Widget{
     
public:
	Widget(int i,bool b);
	Widget(int i,double d);
	Widget(std::intializer_list<long double> il);

	operator float() const ;   //强制转换成float型别
};

Widget w1(10,true);		//调用第一个构造函数
Widget w2{
     10,true};		//使用大括号,调用第三个构造函数,10和true被强制转换为long double

Widget w3(10,5.0);		//调用第二个构造函数
Widget w4{
     10,5.0};		//使用大括号,调用第三个构造函数,10和5.0被强制转换为long double

Widget w5(w4);		//使用小括号,调用的是复制构造函数
Widget w6{
     w4};		//使用大括号,调用第三个构造函数
					//w4的返回值被强制转换float,而float又被强制转换为long double

Widget w7(std::move(w4));		//使用小括号,调用的是移动构造函数
Widget w8{
     std::move(w4)};		//使用大括号,调用第三个构造,和w6结果相同

这种优先调用是很强烈的,即使最优选的调用std::initializer_list的构造函数无法被调用,编译器还是会选择这个。只有在找不到任何办法把大括号初始化物中的实参转化成std::initializer_list模板中的型别时,编译器才会去检查普通的重载函数。

class Widget{
     
public:
	Widget(int i,bool b);
	Widget(int i,double d);
	Widget(std::initializer_list<bool> il);
};

Widget w{
     10,5.0};		//无法通过编译,无法把10和5.0窄化为bool
class Widget{
     
public:
	Widget(int i,bool b);
	Widget(int i,double d);
	Widget(std::initializer_list<std::string> il);
};

Widget w{
     10,5.0};	//调用第二个构造函数,因为编译器无法把int和double转换成string类型,所以寻找重载函数

对于std::initializer_list还有个小问题,当我们使用一对空大括号来构造一个对象,而该对象既支持默认的构造,也支持带有std::initializer_list型别参数的构造。此时的这对空大括号的意思是“没有实参”而不是“空的std::initializer_list”。如果我们想要传入一个空的std::initializer_list,可以通过把空大括号作为构造函数实参的方式实现,即把一对空大括号放入一对小括号或大括号。

class Widget{
     
public:
	Widget();    //默认构造
	Widget(std::initializer_list<int> il);
};

Widget w1;		//调用默认构造
Widget w2{
     };	//调用默认构造
Widget w3();	//解析语法,变成函数声明而不是调用构造
Widget w4({
     });	//调用带有std::initializer_list参数的构造,传入空的std::initializer_list
Widget w5{
     {
     }};	//同上

  大括号初始化物、std::initializer_list、构造函数重载决议,这些内容不注意会有很大的影响。直接影响到的就是std::vector类。std::vector类中有一个形参中没有std::initializer_list型别的构造函数,它允许我们指定容器的初始尺寸,以及一个初始化时让所有元素拥有的值。但是它还有一个带有一个std::initializer_list型别形参的构造函数,允许我们逐个指定容器中的元素值。如果我们要创建一个元素为数值型别的std::vector,并传递了两个实参给构造函数的话,用小括号还是大括号结果会大相径庭:

//小括号,调用了形参中没有一个具备std::initializer_list型别的构造函数
//结果是创建了一个含有10个元素的std::vector,所有的元素值都是20
std::vector<int> v1(10,20);


//大括号,调用了形参中含有std::initializer_list型别的构造函数
//结果是创建了一个含有2个元素的std::vector,元素的值分别为10和20
std::vector<int> v2{
     10,20};

所以从某种程度上说vector的设计是有缺陷的。我们自己在设计一个类的时候,我们需要意识到自己撰写的一组重载构造函数中只要有std::initializer_list形参,则使用大括号初始化的客户代码只会发现这些构造的重载版本。

你可能感兴趣的:(C++学习笔记,#,C++11/14)