本次测试文件test.cpp
为了避免**RVO(return value optimization)**带来的影响,本次测试用g++编译文件时都会带上-fno-elide-constructors选项来关闭RVO
每次的编译命令均为
1 > g++ test.cpp -o test -fno-elide-constructors
本次测试包含头文件如下
测试所使用的类如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class Entity { public : Entity () { std::cout << "Default constructor\t" << this << std::endl; } Entity (int x) { std::cout << "Parameter constructor\t" << this << std::endl; } Entity (const Entity& other) { std::cout << this << "Copy" << &other << std::endl; } ~Entity () { std::cout << "Destructor function\t" << this << std::endl; } Entity& operator =(const Entity& other) { std::cout << this << " = " << &other << std::endl; return *this ; } };
测试代码及测试结果将根据不同情况分开处理
1 2 3 4 5 6 7 8 9 10 11 void fun (Entity e) { std::cout << "fun()..." << std::endl; } int main () { fun (1 ); return 0 ; }
1 2 3 Parameter constructor 0xc2a39ffbbf fun()... Destructor function 0xc2a39ffbbf
直接用参数传递对象时并不会先产生临时对象,再拷贝构造,而是类似于Entity e = 1隐式使用有参构造。
下面看一下同样的fun()函数但是不同的调用方式会有什么不一样的结果
1 2 3 4 5 Entity fun () { Entity e; return e; }
1 2 3 4 5 6 int main () { Entity e1; e1 = fun (); return 0 ; }
1 2 3 4 5 6 7 8 Default constructor 0xc42fdff88e // e1 Default constructor 0xc42fdff83f // e Copy constructor 0xc42fdff88f // tmp 0xc42fdff88f Copy 0xc42fdff83f // tmp copy e Destructor function 0xc42fdff83f // e 0xc42fdff88e = 0xc42fdff88f e1 = tmp Destructor function 0xc42fdff88f // tmp Destructor function 0xc42fdff88e // e1
1 2 3 4 5 int main () { Entity e1 = fun (); return 0 ; }
1 2 3 4 5 Default constructor 0xf880fff84f // e Copy constructor 0xf880fff89f // e1 0xf880fff89f Copy 0xf880fff84f // e1 copy e Destructor function 0xf880fff84f // e Destructor function 0xf880fff89f // e1
区别就在于e1对象在一开始有没有被创建
第一种情况,先无参构造出了e1,在用等号赋值时产生了临时对象tmp。
第二种情况,e1一开始没有被创建。这种情况下,是隐式的调用拷贝构造函数,等同于Entity e1(fun()),这里应该是没有问题的。我猜测,拷贝构造不会产生临时对象所以这里直接以e作为e1的拷贝对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 Entity fun2 () { Entity e (2 ) ; std::cout << "fun2()..." << std::endl; return e; } int main () { std::cout << "before fun2()..." << std::endl; fun2 (); std::cout << "after fun2()..." << std::endl; return 0 ; }
1 2 3 4 5 6 7 8 before fun2()... Parameter constructor 0xe7aa9ffa9f // e fun2()... Copy constructor 0xe7aa9ffaef // tmp 0xe7aa9ffaef Copy 0xe7aa9ffa9f // tmp copy e Destructor function 0xe7aa9ffa9f Destructor function 0xe7aa9ffaef after fun2()...
只是简单的调用,也会在主函数产生临时对象
下面是比较综合例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 void fun1 (Entity e3) { std::cout << "fun1()..." << std::endl; } Entity fun2 () { Entity e (2 ) ; std::cout << "fun2()..." << std::endl; return e; } int main () { Entity e1 (1 ) ; Entity e2 = e1; std::cout << "before fun1()..." << std::endl; fun1 (e1); std::cout << "after fun1()..." << std::endl; std::cout << "before fun2()..." << std::endl; Entity e4 = fun2 (); e4 = fun2 (); std::cout << "after fun2()..." << std::endl; return 0 ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 Parameter constructor 0xf0d1bffbad // e1 Copy constructor 0xf0d1bffbac // e2 0xf0d1bffbac Copy 0xf0d1bffbad // e2 copy e1 before fun1()... Copy constructor 0xf0d1bffbae // e3 0xf0d1bffbae Copy 0xf0d1bffbad // e3 copy e1 fun1()... Destructor function 0xf0d1bffbae // e3 after fun1()... before fun2()... Parameter constructor 0xf0d1bffb5f // e fun2()... Copy constructor 0xf0d1bffbab // e4 0xf0d1bffbab Copy 0xf0d1bffb5f e4 copy e Destructor function 0xf0d1bffb5f // e Parameter constructor 0xf0d1bffb5f // e fun2()... Copy constructor 0xf0d1bffbaf // tmp 0xf0d1bffbaf Copy 0xf0d1bffb5f tmp copy e Destructor function 0xf0d1bffb5f // e 0xf0d1bffbad = 0xf0d1bffbaf // e4 = tmp Destructor function 0xf0d1bffbaf // tmp after fun2()... Destructor function 0xf0d1bffbab // e4 Destructor function 0xf0d1bffbac // e2 Destructor function 0xf0d1bffbad // e1
接下来看一下,在RVO的情况下会发生什么有趣的事情
编译命令改为:g++ test.cpp -o test
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 Entity fun2 (Entity e) { std::cout << "fun2()..." << std::endl; Entity e3 (2 ) ; std::cout << "e3 Address: " << &e3 << std::endl; return e3; } int main () { Entity e1; std::cout << "before fun2()..." << std::endl; Entity e2 = fun2 (1 ); std::cout << "after fun2()..." << std::endl; std::cout << "e2 Address: " << &e2 << std::endl; return 0 ; }
1 2 3 4 5 6 7 8 9 10 11 Default constructor 0xb95e1ff61e // e1 before fun2()... Parameter constructor 0xb95e1ff61f // e fun2()... Parameter constructor 0xb95e1ff61d // e3 e3 Address: 0xb95e1ff61d Destructor function 0xb95e1ff61f // e after fun2()... e2 Address: 0xb95e1ff61d Destructor function 0xb95e1ff61d // e3 Destructor function 0xb95e1ff61e // e1
发现e2的地址竟然跟e3一样,为什么呢?以下是我的猜测
编译器发现,最后返回的时候,e2跟e3是几乎是同时创建而且完完全全一模一样的,反正e3最后都是要销毁的,那为什么不直接把e3的内存交给e2呢?所以,在生成汇编代码时,并没有给e3分配内存,对e3的操作就是直接在e2的内存上进行的
这种操作只有在类型为对象时会发生,如果是基本数据类型的话就是比较一般的拷贝操作
参考文章 C++ 函数返回对象时并没有调用拷贝构造函数_在初始化列表中未调用拷贝构造函数-CSDN博客
C++:函数返回值与临时变量_.c++中将临时变量作为返回值的写法-CSDN博客
结语 了解临时变量产生的机制,可以让我们在生产中避免不必要的性能开销。对C++的运行机制也会更加了解。此外,我还发现,形参变量的地址与函数中局部变量的地址似乎并不在同一栈帧上。。。(有待考究