• 第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)实际工程开发中需要人为的避开临时对象

  • 相关阅读:
    RUST实践.md
    redis.md
    opencvrust.md
    aws rds can't connect to mysql server on 'xx'
    Foundation ActionScript 3.0 With Flash CS3 And Flex
    Foundation Flash Applications for Mobile Devices
    Flash Mobile Developing Android and iOS Applications
    Flash Game Development by Example
    Actionscript 3.0 迁移指南
    在SWT中非UI线程控制界面
  • 原文地址:https://www.cnblogs.com/hoiday/p/10091909.html
Copyright © 2020-2023  润新知