• 第23课 神秘的临时对象


    1. 有趣的问题

    (1)程序意图:在Test()中以0作为参数调用Test(int i)将成员变量mi初始值设置为0.

    (2)运行结果:成员变量mi的值为随机值(没达到目的!)

          

    【实例分析】有趣的问题   23-1.cpp

    #include <stdio.h>
    
    
    
    class Test
    {
    
    private:
    
        int mi;
    
    
    public:
    
        //带参构造函数
        Test(int i)
        {
    
            mi = i;
    
        }
    
    
    
        //不带参构造函数
        Test()
        {
            //程序的意图是把Test当成普通函数来使用以达到对mi赋值的目的但直接调用构造函数,会将产生临时对象。
            //所以Test(0)相当于对新的临时对象的mi赋初值为0,而不是对这个对象本身mi赋值.
            Test(0);
    
        }
    
        void print()
        {
    
            printf("mi = %d
    ", mi);
    
        }
    
    };
    
    
    
    
    int main()
    {
    
        Test t;
    
        t.print(); //mi并没被赋初始,这里会输出随机值.
    
    
        return 0;
    
    }

    运行结果:

      

    2. 临时对象

    (1)构造函数是一个特殊的函数调用构造函数产生一个临时对象

    (2)临时对象生命期只有一条语句的时间

    (3)临时对象作用域只在一条语句中

    (4)临时对象C++中值得警惕的灰色地带

    【编程实验】解决方案   23-2.cpp

    #include <stdio.h>
    
     
    
    class Test
    
    {
    
    private:
    
        int mi;
    
        //正确的做法,是提供一个用来初始化的普通函数
    
        void init(int i){ mi = i;}
    
    public:
    
     
    
        //带参构造函数
    
        Test(int i)
    
        {
    
            init(i);
    
         }
    
     
    
        //不带参构造函数
    
        Test()
    
        {
    
            init(0);//调用普通的初始化函数,而不是带参的构造函数Test(int i);
    
         }
    
     
    
        void print()
    
        {
    
            printf("mi = %d
    ", mi);
    
        }
    
    };
    
     
    
    int main()
    {
    
        Test t;
    
        t.print(); //mi并没被赋初始,这里会输出随机值
    
     
    
        return 0;
    
    }

    运行结果:

      

    3. 临时对象返回值优化(RVO)

    (1)现代C++编译器在不影响最终执行结果的前提下会尽力减少临时对象的产生

    【编程实验】神秘的临时对象  23-3.cpp

    #include <stdio.h>
    
     
    
    class Test
    
    {
    
    private:
    
        int mi;
    
     
    
    public:
    
     
    
        //带参构造函数
    
        Test(int i)
    
        {
    
            mi = i;
    
            printf("Test(int i): %d
    ", i);
    
         }
    
     
    
        //不带参构造函数
    
        Test()
    
        {
    
            mi = 0; 
    
            printf("Test()
    ");
    
        
    
         }
    
     
    
        //拷贝构造函数
    
        Test(const Test& t)
    
        {
    
            mi = t.mi;
    
            printf("Test(cosnt Test& t): %d
    ", t.mi);
    
        }
    
     
    
        void print()
    
        {
    
            printf("mi = %d
    ", mi);
    
        }
    
     
    
        ~Test(){printf("~Test()
    ");}
    
    };
    
     
    
    Test func()
    
    {
    
        return Test(20);
    
    }
    
     
    
    int main()
    
    {
    
       
    
        Test t = Test(10); //==> Test t = 10,临时对象被编译器给“优化”掉了说明:如果不优化,该行代码的行为:调用Test(10)
    
                           //将产生一个临时对象,并用这个对象去初始化t对象,会先调用Test(int i),再调用Test(const Test& t)
    
     
    
        Test tt = func();  //==> Test tt = Test(20);==>Test tt = 20;
    
                           //说明:如果不优化,该行代码的行为:在func内部调用Test(20),将产生一个临时对象,此时(Test(int i)被调用,然后按值返回,
    
                           //会调用拷贝构造函数Test(const Test&)产生第2个临时对象,最后用第2个临时对象去初始化tt对象,将再次调用Test(const Test& t)
    
     
    
        t.print();
    
        tt.print();
    
     
    
        return 0;
    
    }

    //实际输出(优化后)结果(在g++下,可以关闭RVO优化再测试:g++ -fno-elide-constructors test.cpp)

    //Test(int i): 10

    //Test(int i): 20

    //~Test()

    //~Test()

    (2)返回值优化(RVO)

    //假设Test是一个类,构造函数为Test(int i);
    
    Test func()
    
    {
    
        return Test(2); //若不优化,将产生临时对象,并返回给调用者
    
    }

    返回值优化(RVO):

    ①在没有任何“优化”之前return Test(2)代码的行为这行代码中:

      先构造了一个 Test 类的临时的无名对象(姑且叫它t1),接着把 t1 拷贝到另一块临时对象 t2(不在栈上),然后函数保存好 t2 的地址(放在 eax 寄存器中)后返回,Func的栈区间被“撤消”(这时 t1 也就“没有”了,t1 的生存期在Func中,所以被析构了),在 Test a = TestFun(); 这一句中,a利用t2的地址,可以找到t2,接着进行构造。这样a的构造过程就完成了。然后再把 t2 也“干掉”。

    ②经过“优化”的结果

      可以看到,在这个过程中,t1和t2 这两个临时的对象的存在实在是很浪费的,占用空间不说,关键是他们都只是为a的构造而存在,a构造完了之后生命也就终结了。既然这两个临时的对象对于程序员来说根本就“看不到、摸不着”(匿名对象),于是编译器干脆在里面做点手脚,不生成它们!怎么做呢?很简单,编译器“偷偷地”在我们写的TestFun函数增加一个参数 Test&,然后把a的地址传进去(注意,这个时候a的内存空间已经存在了,但对象还没有被“构造”,也就是构造函数还没有被调用),然后在函数体内部,直接用a来代替原来的“匿名对象”在函数体内部就完成a的构造。这样,就省下了两个临时变量的开销。这就是所谓的返回值优化

    ③编译器“优化”后的伪代码

    //Test a = func(); 这行代码,经过编译优化后的等价伪代码:
    
    //从中可以发现,优化后,减少了临时变量的产生
    
     
    
    Test a;   //a只是一个占位符
    
    func(a);  //传入a的引用
    
     
    
    void func(Test& t) //优化时,编译器在func函数中增加一个引用的参数
    
    { 
    
          t.Test(2); //调用构造函数来构造t对象
    
    }

    4. 小结

    (1)直接调用构造函数将产生一个临时对象

    (2)临时对象性能的瓶颈,也是bug的来源之一

    (3)现代C++编译器尽力避开临时对象

    (4)实际工程开发中需要人为的避开临时对象

  • 相关阅读:
    移动端a标签点击图片有阴影处理
    sublime vue 语法高亮插件安装
    mongodb 命令
    MongoDB给数据库创建用户
    windows32位系统 安装MongoDB
    ES6之主要知识点(十)Proxy
    ES6之主要知识点(九)Set和Map
    ES6之主要知识点(八)Symbol
    ES6之主要知识点(七)对象
    Ueditor 1.4.3 插入表格后无边框无颜色,不能正常显示
  • 原文地址:https://www.cnblogs.com/hoiday/p/10091909.html
Copyright © 2020-2023  润新知