C++关于栈对象返回的问题

本次实验环境

环境1:Win10, QT 5.12

环境2:Centos7,g++ 4.8.5

 

一. 主要结论

  可以返回栈上的对象(各平台会有不同的优化),不可以返回栈对象的引用。

 

二.先看看函数传参

C++中,函数传参,可以通过值传递,指针传递,引用传递。

1) 函数参数,参数是类,通过值传递方式。下面通过代码实践一下

main()函数将生成的对象aa传入foo()函数, 相关代码如下 

 1 #include <iostream>
 2 
 3 using namespace std;
 4 
 5 class July
 6 {
 7 public:
 8     July()
 9     {
10         cout<<"constructor "<<this<<endl;
11     }
12 
13     July(const July &another)
14     {
15         cout<<"copy constructor "<<this<<" copy from "<<&another<<endl;
16     }
17 
18     July & operator=(const July &another)
19     {
20         cout<<"operator = "<<this<<" copy from "<<&another<<endl;
21     }
22 
23     ~July()
24     {
25         cout<<"destructor "<<this<<endl;
26     }
27 protected:
28 };
29 
30 void foo(July jj)
31 {
32     cout<<"foo()"<<endl;
33 }
34 
35 int main()
36 {
37     July aa;
38     foo(aa);
39     return 0;
40 }

 运行结果如下

从打印结果看,发生了一次构造,一次拷贝构造,两次析构。

 

2) 函数参数,通过传引用方式传递参数

将上面第30行代码修改为如下

1 void foo(July &jj)

运行结果如下

从结果看来,发生了一次构造,一次析构。比上面少了一次拷贝构造和一次析构。这也验证了,传引用效率更高

 

三.RVO/NRVO

(具名)返回值优化((Name) Return Value Optimization,简称(N)RVO),是这么一种优化机制:当函数需要返回一个对象的时候,如果自己创建一个临时对象返回,那么这个临时对象会消耗一个构造函数的调用、一个拷贝构造函数的调用和一个析构函数的调用的代价。通过优化的方式,可以减少这些开销。Windows和Linux的RVO和NRVO是有区别的。

1) 将main()函数和foo()函数调整如下,foo()函数返回不具名临时对象

 1 July foo()
 2 {
 3     cout<<"foo()"<<endl;
 4     return July();
 5 }
 6 
 7 int main()
 8 {
 9     foo();
10     return 0;
11 }

执行结果如下

 从打印结果看,执行了一次构造函数和一次析构函数。

 

2) 去掉平台优化

在QT工程的.pro文件中,增加如下代码

1 QMAKE_CXXFLAGS += -fno-elide-constructors

保存。清除,重新构建。再次运行

  从打印结果看,执行了一次构造,一次拷贝构造,两次析构。

 

3) 再看看具名返回值优化

将foo函数调整如下,先创建一个对象jjq,再将其返回。main函数不变,仍然是调用foo函数。

1 July foo()
2 {
3     cout<<"foo()"<<endl;
4     July jjq;
5     return jjq;
6 }

运行结果如下

 从结果看,执行了一次构造,一次析构。

 

4) 去掉平台的优化,如上面解除优化的步骤一致

 从运行结果看,执行了一次构造,一次拷贝构造,两次析构。与步骤2)中情况是一样的。

 

优化本质:

在main()函数调用foo()之前,会在自己的栈帧中开辟一个临时空间,该空间的地址作为隐藏参数传递给foo()函数,在需要返回A对象的时候,就在这个临时空间上构造一个A a。然后这个空间的地址再利用寄存器eax返回给main(),这样main()函数就能获得foo()函数的返回值了。

 如果有人会汇编的话,可以通过反汇编观察一下调用情况。目前我不怎么会汇编,以后再抽时间看下汇编。

 

四.接收栈对象

接收栈对象的方式不同,会影响优化。

在没有去除平台优化的情况下,再次测试

1) foo()函数和main()函数调整如下,foo()函数中返回不具名对象,main()函数中通过foo()函数返回的对象来构造first对象

 1 July foo()
 2 {
 3     cout<<"foo()"<<endl;
 4     return July();
 5 }
 6 
 7 int main()
 8 {
 9     July first = foo();
10     return 0;
11 }

运行结果如下

从打印结果看,执行了一次构造函数和一次析构函数。

 

2) 调整main()函数,先创建一个对象first,再将foo()函数返回的对象赋值给刚刚创建的对象first,代码如下

 1 July foo()
 2 {
 3     cout<<"foo()"<<endl;
 4     return July();
 5 }
 6 
 7 int main()
 8 {
 9     July first;
10     first = foo();
11     return 0;
12 }

运行结果如下

从打印结果看,执行了两次构造函数,一次拷贝赋值函数,两次析构函数。比上面多了一次构造函数、一次拷贝赋值函数、一次析构函数。

 

3) foo()函数和main()函数调整如下,foo()函数中返回具名对象jjq,main()函数中通过foo()函数返回的对象来构造first对象。

 1 July foo()
 2 {
 3     cout<<"foo()"<<endl;
 4     July jjq;
 5     return jjq;
 6 }
 7 
 8 int main()
 9 {
10     July first = foo();
11     return 0;
12 }

运行结果如下

 执行了一次构造函数和一次析构函数。

 

4) 调整main()函数,先创建一个对象first,再将foo()函数返回的对象赋值给刚刚创建的对象first,代码如下

 1 July foo()
 2 {
 3     cout<<"foo()"<<endl;
 4     July jjq;
 5     return jjq;
 6 }
 7 
 8 int main()
 9 {
10     July first;
11     first = foo();
12     return 0;
13 }

运行结果如下

从打印结果看,执行了两次构造函数和一次拷贝赋值函数,两次析构函数。具名与不具名是一样的情况。

所以,在接收栈对象时,直接构造新对象即可。而不必要分两步,先创建对象,再赋值,相对效率较低。

 

五.可否返回栈上对象的引用

其实想传达的主要是这个问题,一下子就扯了那么多。

向函数传递引用,相当于扩展了对象的作用域,使用起来比较方便。但是栈上生成的对象的引用,可以返回吗?验证一下。

main()函数和foo()函数调整如下,foo()函数返回的是引用

 1 July & foo()
 2 {
 3     cout<<"foo()"<<endl;
 4     July jjq;
 5     return jjq;
 6 }
 7 
 8 int main()
 9 {
10     July first = foo();
11     return 0;
12 }

执行结果如下

从打印结果可以看到,在foo()函数中,生成的对象jjq在离开foo()函数时已经进行了析构。在main()函数中,对象first由一个空地址进行构造,这个first对象因此没有正常进行构造。没有挂掉,可能与平台有关系 。下面,把这份代码拷贝到Linux平台,再验证一下

编译时,有告警产生:"返回了局部变量的引用"。

这也说明了,可以返回栈上的对象(各平台会有不同的优化),不可以返回栈对象的引用。

 

参考资料

王桂林  《C++基础与提高》

posted @ 2021-09-20 16:11  bruce628  阅读(688)  评论(0编辑  收藏  举报