指定初始化值的方式包括使用小括号、使用等号、或使用大括号:
int x(0); //初始化物在小括号内
int y = 0; //初始化物在等号之后
int x{0}; //初始化物在大括号之内
对于int
这样的内置类型来说,初始化和赋值无区别,但是对于用户自定义的类型,初始化和赋值就有区别了:
Widget w1; // 调用的是默认构造函数
Widget w2 = w1; // 并非赋值,调用的是复制构造函数
w1 = w2; // 并非赋值,调用的是赋值运算符
大括号同样可以用来为非静态成员制定默认初始化值,也可以使用“=,却不能使用小括号
class Widget {
private:
int x{0};
int y = 0;
int x(0); // 无法通过编译
};
不可以复制的对象(如std::atomic
类型的对象)可以使用大括号和小括号进行初始化,却不能使用“=”:
std::atomic<int> ai1{0}; // 正确
std::atomic<int> ai2(0); // 正确
std::atmoic<int> ai3 = 0; // 错误
大括号初始化有一项新特性,就是它禁止内建类型之间进行隐式窄化类型转化。如果大括号内的表达式无法保证能够采用进行初始化的对象来表达,则代码不能通过编译;
double x,y,z;
int sum1{x + y + z}; // 错误!double类型之和可能无法用int表达
而采用“小括号”和“=”的初始化则不会进行窄化类型转换检查,因为如果那样的话就会破坏太多遗留代码;
int sum2(x + y + z); // 没问题(表达式被截断为int)
int sum3 = x + y + z; // 同上
C++
规定:任何能够解析为声明的到要解析为声明,而这会带来副作用。比如程序员本来想要以默认方式构造一个对象,结果却不小心声明了一个函数:
当你想以传参方式调用构造函数时,可以这样写:
Widget w1(10); // 调用Widget的构造函数,传入形参10
但如果你试图用对等语法来调用一个没有形参的Widget
构造函数的话,那结果却变成声明了一个函数而非对象:
Widget w2(); // 这个语句声明了一个名为w2,返回一个Widget类型对象的函数
由于函数声明不能使用大括号来制定形参列表,所以用大括号来完成对象的默认构造没有上述问题;
Widget w3{}; // 调用没有形参的Widget构造函数
在构造函数被调用时,只要形参没有任何一个具备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
类型的形参,那么采用了大括号初始化语法的调用语句会强烈地优先选用带有std::inittializer_list
类型形参的重载版本。
class Widget {
public:
Widget(int i, bool b);
Widget(int i, double d);
Widget(std::initializer_list<long double> il);
};
Widget w1(10, true); // 调用的是第一个构造函数
Widget w2{10, true}; // 调用的是第std::initializer_list类型参数的构造函数(10和true被强制转换为long double)
Widget w3(10, 5.0); // 调用的是第二个构造函数
Widget w4{10, 5.0}; // 调用的是第std::initializer_list类型参数的构造函数(10和5.0被强制转换为long double)
即使是平常会执行复制或移动的构造函数也有可能被带有std::initializer_list
的类型形参的构造函数劫持;
class Widget {
public:
Widget(int i, bool b);
Widget(int i, double d);
Widget(std::initializer_list<long double> il);
operator float() const; // 强制转换为float类型
};
Widget w5(w4); // 调用复制构造函数
Widget w6{w4}; // 调用的是第std::initializer_list类型参数的构造函数(w4的返回值被强制转换为float,随后float又被强制转换成long double)
Widget w7(std::move(w4)); // 调用移动构造函数
Widget w8{std::move(w4)}; // 调用的是第std::initializer_list类型参数的构造函数(w4的返回值被强制转换为float,随后float又被强制转换
编译器想要把大括号初始化物匹配带有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}; // 错误!要求窄化类型转换
编译器会忽略前两个构造函数, 转而尝试第三个构造函数,然而要调用改构造函数就要求把int(10)
和double(5.0)
强制转化为bool
类型。而这两个强制转化类型都是窄化的(bool
无法精确表示这两个值中的任何一个),并且窄化类型转化在大括号初始化物内部都是禁止的。
只有在找不到任何办法把大括号初始化物中实参转化为std::initializer_list
模板中的类型时,编译器才会退而去检查普通的重载决议;如比:
class Widget {
public:
Widget(int i, bool b);
Widget(int i, double d);
Widget(std::initializer_list<std::string> il);
};
Widget w1(10, true); // 调用的是第一个构造函数
Widget w2{10, true}; // 调用的是第一个构造函数
Widget w3(10, 5.0); // 调用的是第二个构造函数
Widget w4{10, 5.0}; // 调用的是第二个构造函数b
有个边界用例需要提及,假定你用一对空括号来构造一个对象,而该对象既支持默认构造函数,又支持带有std::intializer_list
类型参数的构造函数,这时候该调用哪个呢?语言规定,在这种情形下应该执行默认构造函数。空大括号对表示的是“没有实参”,而非空的std::initializer_list
:
class Widget {
public:
Widget(i);
Widget(std::initializer_list<int> il);
};
Widget w1; // 调用默认构造函数
Widget w2{}; // 调用默认构造函数
Widget w2(); // 令人苦恼,变成了函数声明语句
如果你的确想要调用一个带有std::initialzier_list
类型参数的构造函数,并传入一个空的std::initizlizer_list
的话,你可以通过把空大括号对作为构造实参的方式实现这个目的;
Widget w4({});
Widget w5{{}};