首页 > 代码库 > C++11中的变量初始化
C++11中的变量初始化
变量初始化很简单嘛,有什么难的?
打住,不要骄傲,往下看,你会哭的。
请看下面四个问题:
1: 下面的语句有不同吗?不同在哪里?
widget w; // a
widget w(); // b
widget w{}; // c
widget w(x); // d
widget w{x}; // e
widget w = x; // f
widget w = {x}; // g
auto w = x; // h
auto w = widget{x}; // i
2: 下面两行语句分别作了什么?
vector<int> v1(10, 20); // a
vector<int> v2{10, 20}; // b
3: 经过第一题与第二题的洗礼,请你告诉我用{}来初始化对象有什么好处?
4: 什么情况下应该用(),什么情况下应该用{},请辩证撕逼!
我知道当你看到第一题的时候已经想把<C++ Primer>撕了糊墙了。
但糊墙之前至少死个明白,弄清楚这四个题的(参考)答案是什么。
解答如下:
在公布答案之前,首先要明白为什么我要提这四个问题。
看起来这只是一个关于茴字到底有几种写法的酸臭题,屏幕面前的观众至少有30%的人会认为这TM有什么用?老子天天写带class的C也挣工资,破语言细节挖那么细有什么用?
请剩下的70%的同学,和我一起对上面提到的30%的人竖起中指。 fucking pussy!
上面的四道题,其实有以下考量:
1: 弄清楚默认初始化,直接初始化,拷贝初始化,列表初始化四种初始化方式的异同
2: 弄清楚用()初始化与用{}初始化的异同
3: 上面问题中的代码中,有一行根本不是初始化!
搞清楚这四道题的终极目标是总结出简洁的定律(在第四题的答案中),彻底搞清楚有关初始化的一切。
下面,解答正式开始。
第一题:
widget w;
这是标准的默认初始化。
这条语句声明了一个变量,其名为w,其类型为widget。
1: 对于大多数情景,该变量被默认构造函数widget::widget()所构造。
2: 若widget是一个内置类型,则w未被初始化,其值是未定义的
3: 若widget是一个简陋的类,那么w将被由编译器生成的默认构造函数所构造。
所谓简陋,是指:widget没有显式声明定义任何构造函数
widget w();
这是一个屎坑,来自于C++臭名昭著的“历史原因”。在现代C++项目中,应极力避免这种写法。
直观上看,这好像也是一个初始化语句,声明了一个变量,其名为w,其类型为widget,并显式的调用了widget的无参(默认)构造函数widget::widget()
但其实并不是。注意看下面的语句
int func();
看三遍。
请告诉我,你觉得这是什么?
对的,这是一个函数声明,显然
widget w();
也是一个函数声明。是的,如果一条语句看起来像是一个函数声明,那么你不能假定编译器不会认为这是一个函数声明。
事实上,这个屎坑每天都被无数人踩来踩去,注意看下面的代码,这在实际项目中也是非常频繁出现的写法:
// 注意, gadget和doodad是两个类型名
widget w( gadget(), doodad() );
你以为你初始化并构造了w,但事实上,你写的是一个函数声明。。
不信?敲下面的代码试试:
#include <iostream>
struct A { int a; };
struct B { int b; };
class C {
public:
int a;
int b;
public:
C(A a, B b) : a(a.a), b(b.b) { }
};
C c1(A(), B()); // 这被编译器认为是一个函数声明
C c2{ A(), B() }; // 这被编译器认为是一个全局变量的声明+初始化
int main(void) {
std::cout << c1.a << c1.b << std::endl; // 编译错误提示应该在这一行,因为c并不是一个C实例,所以没有.a成员
std::cout << c2.a << c2.b << std::endl;
system("pause");
return 0;
}
在C++11中,你不应该写出这种有二义性的语句。
widget w{};
这是一个清晰的,令人愉悦的,没有任何二义性的,初始化语句。
这就是使用{}来进行初始化所带来的第一个好处:用{}来替代(),变量声明初始化就是声明初始化,函数声明就是函数声明,大家井水不犯河水!
这条语句:声明了一个变量,其名为w,其类型为widget,该变量使用widget的默认构造函数widget::widget()构造
有好事者,言道:“非也!非也!如果widget有一个参数为std::initializer_list的构造函数,难道这条语句不会导致编译器调用那个构造函数吗?”
不,并不会,C++11标准中明确了,当{}内容为空时,将调用默认构造函数。
widget w(x);
widget w{x};
这两行是直接初始化,上面我们谈到的都是默认初始化。
大家有没有想过,为什么叫“直接”初始化呢?
因为,假定x不是一个类型名,那么上面两行都是显式的构造函数调用语句,逻辑上效果是widget::widget(x)。
这里,有两点需要注意
1: 且当x恰好就是widget的实例时,将调用拷贝构造函数。
2: 当使用{x}形式时,如果widget定义了一个参数类型为initializer_list的构造函数,那么将调用这个构造函数。
如果widget并没有定义这样的构造函数,那么将调用最可能被调用的那个构造函数
之所以说最可能,是因为传递参数给函数调用,实参类型并不一定需要精确匹配形参类型,x可能会经过一次类型转换
那么,这两种写法有什么区别呢?
区别1:
widget w(x); 这种写法还是有歧义的,当x恰好还是一个类型名时,即使在当前作用域有一个变量名也是x,这种写法还会被认为是一个函数声明
widget w{x}; 这种写法永远不会被认为是函数声明
区别2:
widget w{x}; 的类型检查与类型转换将更严格,这种写法不允许有精度损失的类型转换,如下所示:
int i1( 12.3456 ); // 正确,因为()写法允许精度丢失的类型转换
int i2{ 12.3456 }; // 错误,因为{}的写法不允许类型转换时出现精度丢失
widget w = x;
widget x = {x};
这两行都是拷贝初始化,前者是拷贝初始化,后者是拷贝列表初始化
这两行语句将调用widget的拷贝构造函数或移动构造函数(不知道移动构造函数为何物的,请参阅中文版<C++ Primer>第五版13.6节,470页,英文版531页)
注意:既然是调用构造函数,那么就可能存在隐式的参数类型转换!
这里再老生常谈一个注意点,请将下面这句话念三遍:
这不是赋值!这不是赋值!这不是赋值!
是的,这两行语句中有=操作符出现,但这真的不是赋值,这是初始化!
初始化调用的是构造函数,即widget::widget(x),而赋值调用的是赋值操作符,是widget::operator=(x)
如果真的不明白为什么里出现了=但不是赋值,请翻书温习基础。
下面是要注意的点:
1: 如果x恰好是widget的对象,那么就是直接调用拷贝构造函数或者移动构造函数,两行语句效果一致。
2: 对于widget w = x;而言,如果x并不是widget的对象,!!!理论上!!!,将执行下面的步骤
1: 编译器首先用x初始化并构造一个widget类型的临时变量,其构造动作相当于直接初始化,类似于下面的语句
widget temp{x};
需要注意的是,编译器构造的这个临时变量其实是一个右值变量
2: 然后编译器将使用这个右值变量,试图调用widget的移动构造函数,临时右值变量阅后即焚,如果没有移动构造函数,将调用拷贝构造函数。
如果存在从x到widget的隐式转换,那么widget w = x的效果等同于widget w(widget(x));
注意,这只是理论上的步骤,实际实现上,编译器到底怎么做的,并不确定。在存在类型转换操作符的情况下,可能直接一步到位使用隐式类型转换,只调用一次构造函数。
类似于
widget temp(widget(x)); // 注意,内层的widget(x)是对类型转换操作符的调用
唯一确定的就是:这两行语句都将调用拷贝构造函数或移动构造函数,并且无论如何,请保持拷贝构造函数是存在的,可用的。
而对于widget x = {x};而言,这叫做拷贝列表初始化。实际上效果和widget w{x};一毛一样,没有任何区别。
auto w = x;
auto w = widget{x};
这两行也是拷贝初始化,但更容易理解一点。
并且对于auto w = widget{x};来讲,无论从x到widget是显式的转换,还是隐式的转换,效果都是相同的。
注意点:
1: auto w = x;妥妥的调用拷贝构造函数,因为w的类型是推断出来的,所以变成了下面这种
type_of_x w = x; ===> type_of_x w(x);
2: auto w = widget{x};注意{}将搞一个initializer_list出来,所以如果存在参数为initializer_list的构造函数,这个构造函数将被优先调用
3: 对于auto w = widget{x};来讲,如果x就是widget类型的对象,那么只会调用一次构造函数,优先调用移动构造函数,无移动构造函数将调用拷贝构造函数,效果等同于
widget w = widget{x}; ===> widget w{x};
而如果x并不是widget类型的对象,那么就势必会发生类型转换,!!!理论上!!!将调用两次构造函数,实际上调用几次我们不得而知,依赖于编译器的具体实现
这是最具C++11的写法,也是最推荐的写法,也就是说,在用C++11的项目中,如果需要用一个对象x,去初始化一个变量w,那么最推荐这种写法
1: 如果w的类型和x一致,那么请使用
auto w = x;
类型推断将保证w与x类型一致,带来的后果就是肯定会调用拷贝构造函数
2: 如果w的类型和x并不一致,那么请使用
auto w = type_of_w{x};
等号右边保证了无论是显式类型转换还是隐式类型转换,行为都是一致的,无需考虑转换到底是显式的还是隐式的。
关于类型转换的“显式”和“隐式”,请未看过C++11的同学参考中文版<C++ Primer>第五版14.9节,514页,英文版579页
上文中凡是提到类型转换的部分,对类类型而言,隐式类型转换都指普通的类型转换操作符重载,显式类型转换指带explict关键字的类型转换操作符重载
关于第一题的小总结:
1: 默认初始化,请使用下面的写法
type w;
请抛弃下面的写法
type w();
2: 构造的原料与期待的产品类型相同的情况下进行直接初始化,请使用下面的写法
type w{x};
auto w = x;
并至少保证拷贝构造函数可用
3: 构造的原料与期待的产品类型不同的情况下,请使用下面的写法
auto w = type_of_w{x};
保险起见,请保证:
1: 从type_of_x到type_of_w的(拷贝)构造函数可用
2: 从type_of_w到type_of_w的拷贝构造函数可用
3: 从type_of_x到type_of_w的类型转换操作符可用
第二题:
vector<int> v1(10, 20); 调用的是 vector( size_t n, const int& value );
vector<int> v2{10, 20}; 调用的是 vector( initializer_list<int> values );
这两个语句都调用了构造函数,都是直接初始化的语法。
纠结的点在于,vector有多个构造函数,且有两个构造函数符合两个实参的调用,那么,何种情况下调用哪个构造函数呢?
规则如下:
1: {/* 至少两个参数 */}语法将搞出一个initializer_list
2: 形参为initializer_list的构造函数,将优先于普通参数列表的构造函数被调用
所以:
vector<int> v1(10, 20);
assert(v1.size() == 10);
vector<int> v2{10, 20};
assert(v2.size() == 2);
第三题:
使用{}是更严格的语法,没有二义性,使用{}的初始化可被称为“具有一致性的初始化语法”
所谓一致性是指:
1: 对于所有类型,都可以使用{}语法,语法层面上是统一的。包括简单的聚合结构体,数组,标准库容器,以及普通的类
2: 没有二义性,说这是一个初始化,这TM就是一个初始化!!!这不是函数声明!
下面是一致性美学欣赏时间:
struct mystruct { int x, y, z; };
// C++98中的糊屎语法
rectangle w( origin(), extents() ); // 函数声明?初始化?分不清
complex<double> c( 2.71828, 3.1415926 ); // 没有毛病
mystruct m = {1, 2}; // 没有毛病
int a[] = {1, 2, 3, 4}; // 没有毛病
vector<int> v; // 有毛病,为了初始化一个简单的向量,竟然要写两行!不能忍!
for(int i = 0; i < 4; ++i) v.push_back(i);
// 总的来看,初始化语法乱七八糟一锅粥
// C++11里的一致性美感
rectange w = { origin(), extents() }; // 漂亮
complex<double> c = { 2.71828, 3.14159 }; // 美
mystruct m = { 1, 2 }; // 赏心悦目
int a[] = { 1, 2, 3, 4 }; // 简直享受
vector<int> v = { 1, 2, 3, 4 }; // 啊。。长江。。啊。。黄河!
其实,你或许会说美感都是红粉骷髅,没啥意思,那么你看看下面的泛型代码(看不懂拉倒,我知道很多人并不了解泛型模板,包括我(译者)在内)
template<typename T, typename ...Args>
void forwarder( Args&&... args ) {
// ...
T local = { std::forward<Args>(args)... };
// ...
}
forwarder<int> ( 42 ); // ok
forwarder<rectangle> ( origin(), extents() ); // ok
forwarder<complex<double>>( 2.71828, 3.14159 ); // ok
forwarder<mystruct> ( 1, 2 ); // 错误
forwarder<int[]> ( 1, 2, 3, 4 ); // 错误
forwarder<vector<int>> ( 1, 2, 3, 4 ); // 错误
并且,并且,一致性到了成员初始化器都可以使用{}语法
widget::widget(/* ... */) : mem1{init1}, mem2{init2, init3} { /* ... */ }
并且,在函数调用传递参数、返回值方面,{}也能带给你很多便利
void draw_rect(rectangle);
draw_rect(rectangle(origin, selection)); // C++98
draw_rect({origin, selection}); // C++11
rectangle compute_rect() {
// **
if(cpp98)
return rectangle(origin, selection); // C++98
else
return {origin, selection}; // C++11
}
第四题:
0: 调用默认构造函数,你应该始终使用{},例如:
widget w{};
1: 要用多个参数搞定的初始化,你应该始终使用{},例如:
vector<int> v = {1,2,3,4};
auto v = vector<int>{1,2,3,4};
因为{}有上面说过的很多好处,诸如简洁,没有二义性等
2: 只用一个参数就能搞定的初始化,你可以省略掉{},转而使用=,例如:
int i = 42;
auto x = anything;
当然,强迫症患者可以从一而终,始终使用{},例如:
int i{42};
auto x{anything};
3: 只有需要调用特定的构造函数时,才需要在初始化中使用(),如:
vector<int> v1(10, 20);
auto v2 = vector<int>(10, 20);
最后一点注意:当你设计一个类时,尽量避免让一个普通构造函数与一个参数带initializer_list的构造函数发生冲突
C++11中的变量初始化