本次实验环境
环境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++基础与提高》