C++对象拷贝时的优化编译
在现代编译器中,当我们在 C++中进行对象的拷贝操作时,编译器并非只是机械地执行逐字节的复制。相反,它会进行优化,避免不必要的拷贝构造等等,这种优化包括“返回值优化”(RVO),“拷贝省略” 等等。以下代码运行结果是在vs2022下实现的。
#include<iostream>
using namespace std;class A
{
public:A(int a = 0):_a1(a){cout << "A(int a)" << endl;}//拷贝构造A(const A& aa):_a1(aa._a1){cout << "A(const A& aa)" << endl;}A& operator=(const A& aa){cout << "A& operator=(const A& aa)" << endl;if (this != &aa){_a1 = aa._a1;}return *this;}//析构~A(){cout << "~A()" << endl;}
private:int _a1 = 1;
};
void f1(A aa)
{}
A f2()
{A aa;return aa;
}
1.内置类型转换为自定义类型的优化
int main()
{A aa = 1;return 0;
}
在语法上来看,也就是没有优化的情况下,(1)编译器先调用默认构造函数,在栈上创建临时对象,传入参数值1初始化临时对象的成员变量,(2)再将这个临时对象去拷贝构造给aa,(3)aa被初始化成功后析构这个临时对象,(4)等到程序结束时再析构aa。
从下面的结果来看,也就是优化之后,并没有调用拷贝构造,(1)编译器可能直接调用默认构造函数初始化aa,而不创建临时对象和调用拷贝参数,(2)然后在程序结束时析构aa。
2.传值传参时的优化
void f1(A aa)
{}
int main()
{A aa1;f1(aa1);retrun 0;
}
从语法上看,(1)当执行A aa1时,编译器会调用A类的默认构造函数。由于没有提供参数,默认构造函数A(int n = 0)会被调用,_a1被初始化为0。(2)随后执行f1(aa1),编译器会调用A类拷贝构造函数将aa1的值拷贝到临时对象aa中,(3)然后进入函数f1, 该函数结束时调用析构函数析构aa,(4)最后main函数结束时析构aa1。从结果上来看,此处没有优化。
3.隐式类型
void f1(A aa)
{}
int main()
{ //隐式类型,连续构造+拷⻉构造->优化为直接构造f1(1);return 0;
}
从语法上来看,(1)当调用f1(1)时,编译器调用A的默认构造函数将整数1隐式转换为A类对象。(2)进入函数f1,由于f1的参数是按值传递的,A类的临时对象aa被创建,并且刚刚创建的对象会被拷贝构造到临时对象aa中,(3)当函数f1结束时,析构aa。当main函数结束时,此处由于没有其它对象,所以没有再调用析构函数。
优化过之后,(1)编辑器可能直接会直接在f1的参数位置调用默认构造函数,使用参数1进行构造,(2)而不是先创建一个临时对象再拷贝,因此没有拷贝构造。(3)在f1内部,aa直接引用构造函数,到函数f1结束时,析构aa。得到了下面的结果
4.传值返回
A f2()
{A aa;return aa;
}
int main()
{// 传值返回// 返回时⼀个表达式中,连续拷⻉构造+拷⻉构造->优化⼀个拷⻉构造 (vs2019 debug)// ⼀些编译器会优化得更厉害,进⾏跨⾏合并优化,直接变为构造。(vs2022 debug)//构造+拷贝构造-》直接构造A aa2 = f2();
}
从语法上来看,(1)当调用A aa2 = f2();时,程序进入f2函数,首先创建局部对象aa,调用默认构造函数初始化aa,(2)返回时会触发拷贝构造函数,创建一个临时对象用于返回值传递,(3)在main函数中临时对象初始化aa2再次触发拷贝构造,(4)当f2函数执行完毕,编译器会调用aa的析构函数,(5)当main函数结束时调用aa2的析构函数。
优化后,(1)在函数f2中调用了默认构造函数创建一个A类对象aa2,(2)从结果上来看,编译器可能是只创建了aa2,将aa类似一个指针的效果指向aa2,因此只在在main函数结束时调用了析构函数析构aa2,而没有析构aa。
A f2()
{A aa;cout << &aa2 << endl;return aa;
}
int main()
{cout << &aa2 << endl;A aa2 = f2();
}
从下面的图中我们可以看到,aa和aa2的地址是相同的。
这篇文章主要目的是为了介绍编译器的对代码的优化,以及学习程序运行的语法,可能有表述不严谨的地方,欢迎大家探讨纠正。