编译器对对象拷贝的优化
2026/7/24 20:35:05 网站建设 项目流程

目录

摘要:

一:优化规则

二: 优化展示

1:在一行中的构造+拷贝

情况1:

情况2:

情况3:

2:不在一行中的拷贝+拷贝

3:不在一行中的构造+拷贝

4:不在一行的的构造+构造

三:思考


摘要:

在 C++ 中,当类对象连续触发构造和拷贝(或两者组合)时,编译器可能会在不改变程序语义的前提下,省略某些构造或拷贝调用,以提升性能。这种优化会影响构造、拷贝和析构函数的实际执行次数。

因此,如果我们在这些特殊成员函数中插入打印语句,输出的次数可能与我们预期的逻辑次数不符。理解这种优化机制,是正确解读打印结果的前提。

本文将在 Visual Studio 2019 Debug 环境下进行测试,重点在于建立“编译器优化确实存在且会影响行为”的认知——因为不同编译器、不同优化级别下的具体表现可能各不相同,但优化本身是普遍存在的。

最后解读编译器究竟做了什么,才既能保证程序的正确允许,又达到了优化的效果

本篇文章涉及到临时变量机制和权限,所以建议先看:C++中的引用


一:优化规则

Visual Studio 2019 Debug 环境下下,会产生以下优化:

如下:

①:构造+构造->构造

②:构造+拷贝构造->构造

③:拷贝构造+拷贝构造->拷贝构造

再怎么优化,你都会保留构造,因为如果对象都没有构造出来,谈何后面的拷贝?

注意:不同的编译器优化的效果可能会不同(博主是在 VS19 下的 Debug 版本下进行演示)


二: 优化展示

以下是一个用于测试的类A,其有构造函数,拷贝函数,赋值重载函数,构析函数,进入每个函数都会打印调用了一次 xx函数 的字眼,这样更能显式的看出调用的次数,以体现优化

class A { public: //构造函数 A(int a = 0) :_a(a) { cout << "A(int a)调用一次构造" << endl; } //拷贝函数 A(const A& aa) :_a(aa._a) { cout << "A(const A& aa)调用一次拷贝" << endl; } //赋值重载函数 A& operator=(const A& aa) { cout << "A& operator=(const A& aa)调用一次赋值" << endl; if (this != &aa) { _a = aa._a; } return *this; } //构析函数 ~A() { cout << "~A()调用一次构析" << endl; } private: int _a; };

1:在一行中的构造+拷贝

情况1:

int main() { A aa1 = 1; return 0; }

解释:

①:aa1是A类实例化的对象,而1是整形,这叫作类型转换

②:类型转换的规则如下:

③:所有的类型转换都是不会影响到变量本身的,所以如图中就是不会影响到1,并不是1真的变成了一个对象,而是1先作为参数进行了匿名构造,构造出一个临时对象(临时变量都是具有常属性的,即不能被更改),然后这个临时对象再去进行拷贝构造出了aa1(用一个已存在的对象去初始化一个未存在的对象,叫作拷贝构造)

④:所以这里在同一行代码中进行构造+拷贝构造,所以优化成了单次的构造,如下图:

正如前文的规则所言,构造+拷贝构造->构造(构析是aa1出了main函数调用了)

Q:为什么不是直接进行拷贝构造呢?直接让1拷贝构造生成对象aa1不好吗?为什么要生成中间变量?不会压根没有中间变量的生成吧?

A:首先直接让1拷贝构造生成对象aa1这个想法就是错的,因为类型是不匹配的,所以必定会发生类型转换!而类型转换必定涉及到临时变量的产生!不妨反向验证一下!如果没有中间变量的生成,那从1到aa1就应该是变量到变量,那为何以下这样写不行呢?

A& aa1 = 1;

这样直接对生成的aa1进行了引用而已吗?权限是一致,但为什么就不行了呢?

报错如下:

正确写法:

const A& aa1 = 1;

解释:因为我们引用的不是1,而是这个临时变量,而临时变量具有常性,所以为了避免权限放大,所以我们的aa1也要被const修饰,从而和临时变量的权限一致,才不会报错!

情况2:

void func(A aa1) { } int main() { //匿名构造仅是构造(不是类型转换,谈何构造加拷贝),然后再拷贝传参 func(A(2)); return 0; }

解释:

func(A(2)),中的A(2)是一次匿名构造,调用了一次构造函数

A(2)构造出来的对象作为实参会给到func函数的形参,此处值传递,所以调用了一次拷贝函数

所以是一次构造 + 拷贝,但是是在同一行上连续进行的,所以优化成了:

情况3:

void func(A aa1) { } //构造+拷贝构造->构造 int main() { //3 到 aa1 是隐示类型转换 先构造 在拷贝 func(3); return 0; }

解释:3直接作为参数给func函数的aa1接收,这是类型转换,如情况1中的意义,先进行构造,再进行拷贝,所以 应该是 在一行上进行 构造 + 拷贝,优化成了:


2:不在一行中的拷贝+拷贝

A func() { A aa; return aa; } int main() { A aa1 = func(); return 0; }

解释:func函数中 aa是一次构造,return aa,aa会拷贝构造出临时变量,所以这是一次拷贝 ,A aa1 中的 aa1 还未存在,此时去接受 func函数return 出来的对象,这又是一次拷贝,演示后两次拷贝的优化效果

结果:

后两次拷贝优化成了一次


3:不在一行中的构造+拷贝

void func(A aa) { } int main() { A aa1(1); func(aa1); return 0; }

解释:

①:A aa1(1),是一次构造函数的调用

②:func(aa1),将aa1传给了func函数,是一次拷贝函数的调用

③:所以这是 构造 + 拷贝函数 ,但是不会被优化,因为其不是在一行上连续进行的

结果:

两次构析是aa 和 aa1 离开自己的作用域调用的


4:不在一行的的构造+构造

A func() { A aa; return aa; } int main() { A aa2; aa2 = func(); return 0; }

解释:

①:A aa2是调用一次构造

②:func 则进入函数,A aa 是一次构造

③:return aa 是一次拷贝

④:函数返回的对象给 aa2 是一次赋值(两个存咋的对象用 = 叫作赋值)

这个对前两次的构造进行展示,因为不是一行上的连续构造,所以不进行优化

结果:


三:编译器的行为

上述讲解的核心在于建立这样一个认知:编译器会在保证程序语义正确的前提下,对构造和拷贝操作进行优化,以减少不必要的临时对象开销。需要特别注意的是,这种优化并非固定不变——不同的编译环境,甚至同一编译器下的不同优化级别,其优化策略和深度都可能存在差异。某些环境下,优化程度甚至会更激进。因此,我们不必深究具体环境下的优化细节,但必须清楚:优化是真实存在且可能影响程序行为的,这是理解构造/析构调用次数的前提。

下图展示无优化,优化,更深优化,并进行解释:

解释:

①:在g++ -fno-dlide-constructors环境下,是不会作任何优化的

②:而在我们本文的vs2019debug环境下,是会做一定程度的优化

③:而在vs2022的debug环境下,优化程度更深

理解①:

g++ -fno-dlide-constructors环境下是不会作任何优化的,这意味着,临时对象一定会产生,那么临时对象生命周期结束时,也会调用析构函数!而我们上文都是有一定程度的优化的,所以临时变量的析构我们无法观察!

理解②:

在我们上文中,我们知道了return aa,aa会拷贝构造出临时变量,这是第一次拷贝 ,A aa1 中的 aa1 还未存在,此时去接受 func函数return 出来的对象,这是第二次拷贝,这两次拷贝会被优化为一次拷贝;换句话说,不就是aa直接拷贝构造出了aa2吗?

但这是违背了我们所学知识的,因为aa是栈上的临时变量,其会随f2函数栈帧销毁而销毁,怎么做到直接用aa来拷贝构造出main中的aa2呢?这是因为编译器让aa在结束之前就去拷贝构造main中的aa2了,所以不存在类似野指针的问题!

理解③:

vs2022的debug环境下,我们运行代码打印如下:

这意味着,在这种更深程度的优化下,我们连一次拷贝构造函数都没有调用,那问题来了!现在我们变量是aa和aa2,那么这个仅有的构造调用是构造的哪个变量?没有拷贝构造又是怎么生成另一个变量的?所以我们当前暂且认为,仅有的调用的构造函数是指的f2函数中aa的构造,而aa2不知道怎么产生的!

❓️:那编译器到底是怎么做的呢?

💡:编译器做法:直接构造出了aa2,然后让f2中的aa作为aa2的引用!这样只需调用一次构造,其次aa2在后续代码也可以被使用!

❓️:怎么验证呢?

💡:很简单,打印二者地址即可

A f2() { A aa; cout << &aa << endl; return aa; } int main() { A aa2 = f2(); cout << &aa2 << endl; return 0; }

❓️:为什么编译器选择构造main中的aa2,然后让aa作为aa2的引用,而不是构造aa,让aa2作为aa的引用呢?

💡:因为如果构造aa,而aa是栈上的临时变量,你aa2引用一个会随f2函数栈帧销毁而销毁的变量,会出现类似野指针的问题,导致后续aa2的使用会导致报错,所以当然选择构造aa2!

所以正如我们前文所言:“编译器会在保证程序语义正确的前提下,对构造和拷贝操作进行优化,以减少不必要的临时对象开销。”,编译器不管怎么优化,一定都是并且程序语义正确,所以我们担心的问题都不存在,编译器会采取一些特殊的措施,在保证正确前提下,还达到了优化的效果!

📌 [ 作者 ] shylyly
📃 [ 首次发布 ] 2024.10.16
❌ [ 最新修改 ] 2026.7.23
📜 [ 声明 ] 由于笔者水平有限,文中难免有疏漏或不妥之处,还望读者不吝赐教

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询