复杂的C++,当函数返回对象到底发生了什么?

作者: veaxen 分类: C/C++ 发布时间: 2017-06-21 16:39

我们知道,当函数运行结束的时候,函数内部的局部变量就会消失,这C/C++里没有任何疑问的规定,但是今天我在写代码的时候突然就想到了一个相当纠结的问题,那就是当我一个函数返回类型是一个对象的时候,以我当时掌握的知识理解,当函数返回时回要生成一个临时对象,这个临时对象可能会开销很多资源,那么这样我们的函数就不能设计成一个返回类型为对象的函数了,或者想办法避免产生这个临时对象,方法就是使用动态分配内存。出于验证的想法,我写了下面一段代码进行测试:

#include <iostream>
using namespace std;
struct Test{
    Test() {cout<<"Constructor"<<endl;}
    Test(const Test &){cout<<"copy Constructor"<<endl;}
    ~Test() {cout<<"destroy"<<endl;}
};
Test fun()
{
    Test t1;
    cout<<"&t1 is "<<&t1<<endl;
    return t1;
}
int main()
{
    fun();
    cout<<"This is a test!"<<endl;
    return 0;
}

对于上面的代码,我的思考是这样的:
– 首先调用函数fun(),调用构造函数产生t1,输出t1的地址,到达return t1语句,这时调用拷贝构造函数产生一个临时对象,t1析构,临时对象析构,输出”This is a test!”,程序结束。

没错吧?对于上面的思考过程,如果你以前对C++有深入的了解,对这样的思考过程应该是认同的吧

好,我们用g++编译运行一下,我的g++版本是gcc version 4.8.4 (Ubuntu 4.8.4-2ubuntu1~14.04.3),下面是运行结果

Constructor
&t1 is 0x7ffce5a4956f
destroy
This is a test!

不对吧?从这个结果看,好像没有产生临时对象啊,难道是我错了?还是说g++发现我没有使用函数的返回值然后帮我优化了?OK,我就修改一下代码:

...
int main()
{
    Test t2 = fun();
    cout<<"&t2 is "<<&t2<<endl;
    cout<<"This is a test!"<<endl;
    return 0;
}

如果的想法是没有错的,那么过程应该是这样的:
– 首先调用函数fun(),调用构造函数产生t1,输出t1的地址,到达return t1语句,这时调用拷贝构造函数产生一个临时对象,t1析构,调用拷贝构造函数产生对象t2,临时对象析构,输出t2的地址,输出”This is a test!”,t2析构,程序结束。

实际用g++编译运行的结果是:

Constructor
&t1 is 0x7fff5303673f
&t2 is 0x7fff5303673f
This is a test!
destroy

妈呀!这是什么情况?我怎么有点懵逼啊?从这个结果看,只在fun()里调用了一次构造函数,产生t1对象,没有产生临时对象就算了,把我的t2都搞没了是怎么回事?不对,发现一个新问题,t1对象的析构函数怎么变成了在程序结束的时候才调用?不应该是函数fun()结束后就调用的吗?难道又是编译器给优化了?我把优化选项关了,编译时采用的是-O0参数,不过结果还是没有调用拷贝构造函数产生t2,这里我对优化选项了解得不深,所以我采用了代码进行另外的测试。

带着新问题,我又继续写了几段测试代码,测试为什么没有产生t1对象,发现真的是编译器进行了优化(可见-O0选项其实也是对性能进行了一定优化的),我们看看下面这段,编译器不可以采用优化的:

Test fun2(Test const &t)
{
    cout<<"&t3 is "<<&t3<<endl;
    return t;
}
int main()
{
    const Test t3;
    Test t2 = fun2(t3);
    cout<<"&t2 is "<<&t2<<endl;
    cout<<"This is a test!"<<endl;
    return 0;
}

运行的结果是:

Constructor
&t3 is 0x7ffec045a2be
copy Constructor
&t2 is 0x7ffec045a2bf
This is a test!
destroy
destroy

没错,这里调用拷贝构造函数产生了t2了,是编译器优化了之前的那段代码,把原本应该在fun()结束时就析构的t1继续当成了t2使用,提高了效率。

好了,我们回到一开始的问题了,按照之前的理解,函数返回应该是要产生一个临时对象的,我记得在《C++Primer》中都是有提及的,带着这个疑问,我又继续百度了,果然证明我一开始的认知是没有错的,传送门,这是一种叫返回值优化的机制(Return Value Optimization,简称RVO),具体有兴趣的同学去仔细看看。

原来就是g++编译器帮我们优化,以提高性能的,我们可以通过在编译的时候,加上-fno-elide-constructors这个选项,来去除这个优化(再次证明我对g++优化没有一个系统的学习)

我们重新编译最开始的那段程序
g++ -o test test.cpp -fno-elide-constructors
运行:
./test
运行结果:

Constructor
&t1 is 0x7ffeaf3d693f
copy Constructor
destroy
destroy
This is a test!

这下没错了,完全符合一开始的猜想,不过这种产生临时对象的做法效率实在会大大降低,所以g++优化得好啊,不过我们还是要知道编译器帮我们干了这么一回事,以防有什么特殊情况。

写到这里,我又想说了,可能有的人认为不用如此纠结这种问题,但我觉得如果想用真正精通C++,那么我们就必须知道编译器纠结都干了什么,C++之所以那么复杂,那么难,就是因为编译器会帮我们干一些我们可能不知道的事情。

好好学习,天天向上吧!!

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!

发表评论

电子邮件地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据