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

原文地址:https://www.cnblogs.com/hoiday/p/10091909.html