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的构造函数发生冲突

时间: 2024-12-24 20:06:53

C++11中的变量初始化的相关文章

c++ 类与函数中static变量初始化问题(转)

首先static变量只有一次初始化,不管在类中还是在函数中..有这样一个函数: 1 void Foo() 2 { 3 static int a=3; // initialize 4 std::cout << a; 5 a++; 6 } 里的static int a=3只执行了一次.在main中调用Foo()两次,结果为34.将上面的函数改为 1 void Foo() 2 { 3 static int a; 4 a=3; // not initialize 5 std::cout <<

java类中成员变量初始化后存放在堆内存中还是栈内存中?

答案是堆内存. 之前明明看过java类初始化过程的, 但一下子看到这样的题目,还是懵了. 百度后,那些帖子的回复各有各说, 脑袋都看得要塞住了,还是看书求证吧. 李刚的<疯狂Java>第128页开始,有一个类从初始化开始, 在内存发生什么变化的详细过程,这里简单记录一下. class Person{     String name;     static int eyeNum; } 上面这个Person类,有成员变量name和静态成员变量eyeNum了, 当执行下面语句: Person p1 

C++变量初始化问题

初始化和赋值的区别 在C++中,变量初始化和赋值操作符是两个完全不同的概念. 初始化不是赋值,初始化的含义是创建变量分配存储空间时为其赋一个初始值,而赋值的含义是把内存空间的当前值擦除,用一个新值代替. C++中列表初始化 int number1 = 1 int number2 (1) int number3 {1} int number4 = {1} 作为C++11新标准,花括号来初始化变量得到全面应用,这种初始化叫做列表初始化(list initialization) 列表初始化特点:使用列

【Go语言】【5】变量初始化及赋值

在真正的编码过程中要使用一个变量,必须先声明然后才能使用,GO语言也不例外 1.声明变量 var postCode int    //声明一个整型变量postCode var phoneNum int    //声明一个整型变量phoneNum var name string     //声明一个字符串变量name var address string  //声明一个字符串变量address 接下来我们在main()方法直接打印一下各个值分别是多少: 从上面可以看到尽管我们只是声明了一个变量,但

c++11之二: 非静态成员变量初始化

在C++11中,允许非静态成员变量的初始化有多种形式:初始化列表; 使用等号=或花括号{}进行就地的初始化. 可以为同一成员变量既声明就地的列表初始化,又在初始化列表中进行初始化,只不过初始化列表总是看起来“后作用于”非静态成员. 也就是说,初始化列表的效果总是优先于就地初始化的. #include <iostream> 2 using namespace std; 3 class CBase{ 4 public: 5 CBase(){cout<<"mem default

C++中类中常规变量、const、static、static const(const static)成员变量的声明和初始化

C++类有几种类型的数据成员:普通类型.常量(const).静态(static).静态常量(static const).这里分别探讨以下他们在C++11之前和之后的初始化方式. c++11之前版本的初始化 在C++11之前常规的数据成员变量只能在构造函数里和初始化列表里进行初始化.const类型的成员变量只能在初始化列表里并且必须在这里进行初始化.static类型只能在类外进行初始化.static const类型除了整型数可以在类内初始化,其他的只能在类外初始化.代码如下: class A {

C#中对于变量的声明和初始化

C#变量初始化是C#强调安全性的另一个例子.简单地说,C#编译器需要用某个初始值对变量进行初始化,之后才能在操作中引用该变量.大多数现代编译器把没有初始化标记为警告,但C#编译器把它当作错误来看待. 1.在C#中,变量的声明格式为: 数据类型   变量名; 2.变量的赋值格式为: 变量名 = 数据; 3.一般情况下,都是先声明后赋值,或者在声明变量的同时就赋初值.然而有些时候在程序的开发设计中,往往忘了要赋初值(即进行初始化),这样就会导致在程序的设计中,会出现意想不到的错误. 解释:当我们在声

C++中不同变量的初始化规则

当定义没有初始化式的变量(如int i;)时,系统有可能会为我们进行隐式的初始化.至于系统是否帮我们隐式初始化变量,以及为变量赋予一个怎样的初始值,这要取决于该变量的类型以及我们在何处定义的该变量. 1]内置类型变量的初始化     内置变量是否自动初始化,取决于该变量定义的位置.     ①在全局范围内的内置类型变量均被编译器自动初始化为0. #include<iostream> using namespace std; //全局范围内的部分内部变量 int gi; //被自动初始化为0 f

Python中,如何初始化不同的变量类型为空值

参考文章  Python中,如何初始化不同的变量类型为空值 常见的数字,字符,很简单,不多解释. 列表List的其值是[x,y,z]的形式 字典Dictionary的值是{x:a, y:b, z:c}的形式 元组Tuple的值是(a,b,c)的形式 所以,这些数据类型的变量,初始化为空值分别是: 数值 digital_value = 0 字符串 str_value = "" 或 str_value = ” 列表 list_value = [] 字典 ditc_value = {} 元组