23.临时对象

Posted liuyueyue

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了23.临时对象相关的知识,希望对你有一定的参考价值。

不受欢迎的临时对象-------------不知不觉进入你的程序,给程序带来问题

1. 有趣的问题

(1)程序意图

    (1)在Test()中以0作为参数调用Test(int i)

    (2)将成员变量mi初始值设置为0---也就是想代码复用.

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

      技术图片

 1 #include<stdio.h>
 2 
 3 class Test 
 4 {
 5         int mi;
 6 public:
 7     Test(int i)
 8     {
 9         mi = i;
10     }
11     Test()
12     {
13         //程序的意图是把Test当成普通函数来使用以达到对mi赋值的目的但直接调用有参构造函数,会将产生临时对象。
14         //所以Test(0)相当于对新的临时对象的mi赋初值为0,而不是对这个对象本身mi赋值.
15         //临时对象没有名字
16 
17         Test(0);           //是临时对象,声明周期就这一行语句,过了一行就析构
18                            //程序并没有将0设置到初始变量,而是产生临时对象
19                            //等价于空的没有参数的构造函数  Test(){ }
20     }
21 
22     void print() 
23     {
24         printf("mi=%d
",mi);    //mi=456783
25     }
26 
27 };
28 
29 
30 int main()
31 {
32     Test t;
33 
34     t.print();
35 
36     return 0;
37 }
以上到底哪里出了问题???

构造函数是一个特殊的函数

      (1)是否可以直接调用???

      (2)是否可以在构造函数中调用构造函数???

      (3)直接调用构造函数的行为是什么???

2. 临时对象

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

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

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

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

 解决方案:正确的做法,是提供一个用来初始化的普通函数

 1 #include <stdio.h>
 2 
 3 class Test
 4 {
 5 private:
 6     int mi;
 7 
 8     //正确的做法,是提供一个用来初始化的普通函数
 9     void init(int i) 
10     { 
11         mi = i;
12     }
13 
14 public:
15     //带参构造函数
16     Test(int i)
17     {
18         init(i);
19     }
20 
21     //不带参构造函数
22     Test()
23     {
24         init(0);//调用普通的初始化函数,而不是带参的构造函数Test(int i);
25     }
26 
27     void print()
28     {
29         printf("mi = %d
", mi);
30     }
31 };
32 
33 
34 int main()
35 {
36     Test t;
37 
38     t.print(); //mi并没被赋初始,这里会输出随机值
39 
40     return 0;
41 }

以下故意产生临时对象,说明直接调用构造函数的后果:

 1 #include <stdio.h>
 2 
 3 class Test
 4 {
 5 private:
 6     int mi;
 7 
 8     void init(int i)
 9     {
10         mi = i;
11     }
12 
13 public:
14     Test(int i)
15     {
16         printf("Test(int i)
");
17         init(i);
18     }
19 
20     Test()
21     {
22         printf("Test()
");
23         init(0);
24     }
25 
26     void print()
27     {
28         printf("mi = %d
", mi);
29     }
30 
31     ~Test()
32     {
33         printf("~Test()
");
34     }
35 };
36 
37 
38 int main()
39 {
40     printf("main begin");
41 
42     Test();             //直接调用构造函数,产生临时对象,声明周期和作用域都在本行
43     Test(10);           //直接调用构造函数,产生临时对象,声明周期和作用域都在本行
44                        
45     /*打印结果
46                          main begin
47                          Test()
48                          ~Test()
49                          Test(int i)
50                          ~Test()
51                          main end
52    */
53     
54     Test().print();   //生成临时对象,调用print(),合法;临时对象Test()也是合法的对象
55     
56     Test(10).print();
57     
58     /*打印结果
59                          main begin
60                          Test() 
61                          mi=0        
62                          ~Test()
63                          Test(int i)
64                          mi=10
65                          ~Test()
66                          main end
67    */ 
68     
69     
70 
71     printf("main end");
72 
73     return 0;
74 }

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

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

 1 #include<stdio.h>
 2 
 3 
 4 //避开临时函数,直接调用构造函数就会产生一个临时对象
 5 class Test
 6 {
 7 private:
 8     int mi;
 9 public:
10     Test(int i)     //含参构造函数
11     {
12         printf("Test(int i) : %d
", i);
13         mi = i;
14     }
15     Test(const Test& t)    //拷贝构造函数
16     {
17         printf("Test(const Test& t) : %d
", t.mi);
18         mi = t.mi;
19     }
20     Test()      //无参构造函数
21     {
22         printf("Test()
");
23         mi = 0;
24     }
25     void print()
26     {
27         printf("mi=%d
", mi);
28     }
29     ~Test()     //析构函数
30     {
31         printf("~Test()
");
32     }
33 };
34 
35 Test func()
36 {
37     return Test(20);     //生成临时函数,
38 }
39 
40 int main()
41 {
42     Test t = Test(10);  //等价于Test t=Test(10)        Test t(10); 
43                         //分析:将产生一个临时对象,并用这个对象去初始化t对象,会先调用Test(int i),再调用Test(const Test& t))初始化 t 对象  
44                         //用临时对象Test(10)初始化t对象,应该调用构造函数
45                         
46                         //编译器 只是进行了参数构造函数调用  why? 
47                         //为杜绝临时对象产生,等价于====>>Test t=10;少调用一次构造函数,性能提升
48 
49     Test t1 = func();  //初始化t1,只调用Test(int i)    ,没有调用拷贝构造函数,减少临时对象产生
50                        // 等价于Test t1=Test(20);====>>Test t1=20;编译器会自动降低临时对象
51 
52                        //说明:如果不优化,该行代码的行为:在func内部调用Test(20),将产生一个临时对象,此时(Test(int i)被调用,然后按值返回,
53 
54                        //会调用拷贝构造函数Test(const Test&)产生第2个临时对象,最后用第2个临时对象去初始化tt对象,将再次调用Test(const Test& t)
55 
56 
57     Test t1 = 20;
58 
59     t.print();
60     t1.print();
61 
62     return 0;
63 }

//实际输出(优化后)结果(在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)实际工程开发中需要人为的避开临时对象

以上是关于23.临时对象的主要内容,如果未能解决你的问题,请参考以下文章

第23课.神秘的临时对象

第23课 神秘的临时对象

水合没有查询的中继片段

有没有办法使用相同的布局动态创建片段并向它们显示数据?

onActivityResult 未在 Android API 23 的片段上调用

关于将 string::swap() 与临时对象一起使用的问题