首页 > 代码库 > 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有什么用?老子天天写带classC也挣工资,破语言细节挖那么细有什么用?

   

    请剩下的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();

            也是一个函数声明。是的,如果一条语句看起来像是一个函数声明,那么你不能假定编译器不会认为这是一个函数声明。

            事实上,这个屎坑每天都被无数人踩来去,注意看下面的代码,这在实际项目中也是非常频繁出现的写法:

                // 注意, gadgetdoodad是两个类型名

                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的移动构造函数,临时右值变量阅后即,如果没有移动构造函数,将调用拷贝构造函数。

                   如果存在从xwidget的隐式转换,那么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};来讲,无论从xwidget是显式的转换,还是隐式的转换,效果都是相同的。

           

            注意点:

                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;

                   类型推断将保证wx类型一致,带来的后果就是肯定会调用拷贝构造函数

                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_xtype_of_w的(拷贝)构造函数可用

                2: type_of_wtype_of_w的拷贝构造函数可用

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

<style></style>

C++11中的变量初始化