关于重载=运算符中返回Complex还是Complex&问题

来源:8-4 运算符重载

奕帝传说_梦

2023-12-25

不知道还有没有人在马上2024年了还在看这个2020年的课程,但我看了近三年来的问答,感觉关于这个问题都没说到点子上或者说是点到为止了,我这里就给后来者提供一个参考吧…

实际上很简单,确实返回 Complex 会每次都创建一个中间变量,而返回值是 Complex& 则不创建中间变量,直接返回 this 的地址,供后续赋值用!因此 Complex & 的效率肯定会更高,具体分析如下:

我们用一个非常简单的代码来测试:

	Complex c(1.0,2.0);
	Complex b(2.0, 3.0);
	Complex d;
	Complex e, f, g;
	e = f = g = d = b + c;

在这里我强调一下,这里的编译结果是 Windows 环境下,禁用所有优化(不使用 O1/O2)得到的编译结果,不代表所有情况!
首先,我们看看返回值为 Complex& 的情况:

Complex& Complex::operator=(const Complex& x) {	//这里的 & 是定义引用类型
00855560  push        ebp  
00855561  mov         ebp,esp  
00855563  sub         esp,44h  
00855566  push        ebx  
00855567  push        esi  
00855568  push        edi  
00855569  mov         dword ptr [this],ecx  
0085556C  mov         ecx,offset _FE2B3486_Complex@cpp (087C6F4h)  
00855571  call        @__CheckForDebuggerJustMyCode@4 (08520E5h)  
	if (this != &x) {	//这里的 &符号是取地址操作
00855576  mov         eax,dword ptr [this]  
00855579  cmp         eax,dword ptr [x]  
0085557C  je          Complex::operator=+3Eh (085559Eh)  
		_real = x._real;
0085557E  mov         eax,dword ptr [this]  
00855581  mov         ecx,dword ptr [x]  
00855584  movsd       xmm0,mmword ptr [ecx+8]  
00855589  movsd       mmword ptr [eax+8],xmm0  
		_image = x._image;
0085558E  mov         eax,dword ptr [this]  
00855591  mov         ecx,dword ptr [x]  
00855594  movsd       xmm0,mmword ptr [ecx+10h]  
00855599  movsd       mmword ptr [eax+10h],xmm0  
	}
	return *this;	//返回当前对象
0085559E  mov         eax,dword ptr [this]  
}
008555A1  pop         edi  
008555A2  pop         esi  
008555A3  pop         ebx  
008555A4  mov         esp,ebp  
008555A6  pop         ebp  
008555A7  ret         4  
============================上面是operator=,下面是主函数============================
	Complex e, f, g;
0028E88E  lea         ecx,[e]  
0028E891  call        Complex::Complex (0271A4Bh)  
0028E896  mov         byte ptr [ebp-4],4  
0028E89A  lea         ecx,[f]  
0028E8A0  call        Complex::Complex (0271A4Bh)  
0028E8A5  mov         byte ptr [ebp-4],5  
0028E8A9  lea         ecx,[g]  
0028E8AF  call        Complex::Complex (0271A4Bh)  
0028E8B4  mov         byte ptr [ebp-4],6  
	e = f = g = d = b + c;
0028E8B8  lea         eax,[c]  
0028E8BB  push        eax  
0028E8BC  lea         ecx,[ebp-110h]  
0028E8C2  push        ecx  
0028E8C3  lea         ecx,[b]  
0028E8C6  call        Complex::operator+ (02719ABh)  
0028E8CB  mov         dword ptr [ebp-118h],eax  
0028E8D1  mov         edx,dword ptr [ebp-118h]  
0028E8D7  mov         dword ptr [ebp-11Ch],edx  
0028E8DD  mov         byte ptr [ebp-4],7  
0028E8E1  mov         eax,dword ptr [ebp-11Ch]  
0028E8E7  push        eax  
0028E8E8  lea         ecx,[d]  
0028E8EB  call        Complex::operator= (0271B86h)  
0028E8F0  push        eax  
0028E8F1  lea         ecx,[g]  
0028E8F7  call        Complex::operator= (0271B86h)  
0028E8FC  push        eax  
0028E8FD  lea         ecx,[f]  
0028E903  call        Complex::operator= (0271B86h)  
0028E908  push        eax  
0028E909  lea         ecx,[e]  
0028E90C  call        Complex::operator= (0271B86h)  
0028E911  mov         byte ptr [ebp-4],6  
0028E915  lea         ecx,[ebp-110h]  
0028E91B  call        Complex::~Complex (027183Eh) 

重点我们可以看内存为 0x0085559E 的地方,也就是 return *this 这个地方;它直接把 [this] (当前实例化对象的地址) 赋值给了 eax 寄存器;而在 0x0028EBF0 中,我们是直接通过 eax 获取到返回值的引用指针并压栈的!

其次,我们再来看看返回值为 Complex 的情况:

Complex Complex::operator=(const Complex& x) {	//这里的 & 是定义引用类型
00575560  push        ebp  
00575561  mov         ebp,esp  
00575563  sub         esp,48h  
00575566  push        ebx  
00575567  push        esi  
00575568  push        edi  
00575569  mov         dword ptr [this],ecx  
0057556C  mov         dword ptr [ebp-48h],0  
00575573  mov         ecx,offset _FE2B3486_Complex@cpp (059C6F4h)  
00575578  call        @__CheckForDebuggerJustMyCode@4 (05720E5h)  
	if (this != &x) {	//这里的 &符号是取地址操作
0057557D  mov         eax,dword ptr [this]  
00575580  cmp         eax,dword ptr [x]  
00575583  je          Complex::operator=+45h (05755A5h)  
		_real = x._real;
00575585  mov         eax,dword ptr [this]  
00575588  mov         ecx,dword ptr [x]  
0057558B  movsd       xmm0,mmword ptr [ecx+8]  
00575590  movsd       mmword ptr [eax+8],xmm0  
		_image = x._image;
00575595  mov         eax,dword ptr [this]  
00575598  mov         ecx,dword ptr [x]  
0057559B  movsd       xmm0,mmword ptr [ecx+10h]  
005755A0  movsd       mmword ptr [eax+10h],xmm0  
	}
	return *this;	//返回当前对象
005755A5  mov         eax,dword ptr [this]  
005755A8  push        eax  
005755A9  mov         ecx,dword ptr [ebp+8]  
005755AC  call        Complex::Complex (05714D8h)  
005755B1  mov         ecx,dword ptr [ebp-48h]  
005755B4  or          ecx,1  
005755B7  mov         dword ptr [ebp-48h],ecx  
005755BA  mov         eax,dword ptr [ebp+8]  
}
005755BD  pop         edi  
005755BE  pop         esi  
005755BF  pop         ebx  
005755C0  mov         esp,ebp  
005755C2  pop         ebp  
005755C3  ret         8  
============================上面是operator=,下面是主函数============================
	Complex e, f, g;
00A6E8A0  lea         ecx,[e]  
00A6E8A3  call        Complex::Complex (0A51A4Bh)  
00A6E8A8  mov         byte ptr [ebp-4],4  
00A6E8AC  lea         ecx,[f]  
00A6E8B2  call        Complex::Complex (0A51A4Bh)  
00A6E8B7  mov         byte ptr [ebp-4],5  
00A6E8BB  lea         ecx,[g]  
00A6E8C1  call        Complex::Complex (0A51A4Bh)  
00A6E8C6  mov         byte ptr [ebp-4],6  
	e = f = g = d = b + c;
00A6E8CA  lea         eax,[c]  
00A6E8CD  push        eax  
00A6E8CE  lea         ecx,[ebp-128h]  
00A6E8D4  push        ecx  
00A6E8D5  lea         ecx,[b]  
00A6E8D8  call        Complex::operator+ (0A519ABh)  
00A6E8DD  mov         dword ptr [ebp-190h],eax  
00A6E8E3  mov         edx,dword ptr [ebp-190h]  
00A6E8E9  mov         dword ptr [ebp-194h],edx  
00A6E8EF  mov         byte ptr [ebp-4],7  
00A6E8F3  mov         eax,dword ptr [ebp-194h]  
00A6E8F9  push        eax  
00A6E8FA  lea         ecx,[ebp-140h]  
00A6E900  push        ecx  
00A6E901  lea         ecx,[d]  
00A6E904  call        Complex::operator= (0A523A6h)  
00A6E909  mov         dword ptr [ebp-198h],eax  
00A6E90F  mov         edx,dword ptr [ebp-198h]  
00A6E915  mov         dword ptr [ebp-19Ch],edx  
00A6E91B  mov         byte ptr [ebp-4],8  
00A6E91F  mov         eax,dword ptr [ebp-19Ch]  
00A6E925  push        eax  
00A6E926  lea         ecx,[ebp-158h]  
00A6E92C  push        ecx  
00A6E92D  lea         ecx,[g]  
00A6E933  call        Complex::operator= (0A523A6h)  
00A6E938  mov         dword ptr [ebp-1A0h],eax  
00A6E93E  mov         edx,dword ptr [ebp-1A0h]  
00A6E944  mov         dword ptr [ebp-1A4h],edx  
00A6E94A  mov         byte ptr [ebp-4],9  
00A6E94E  mov         eax,dword ptr [ebp-1A4h]  
00A6E954  push        eax  
00A6E955  lea         ecx,[ebp-170h]  
00A6E95B  push        ecx  
00A6E95C  lea         ecx,[f]  
00A6E962  call        Complex::operator= (0A523A6h)  
00A6E967  mov         dword ptr [ebp-1A8h],eax  
00A6E96D  mov         edx,dword ptr [ebp-1A8h]  
00A6E973  mov         dword ptr [ebp-1ACh],edx  
00A6E979  mov         byte ptr [ebp-4],0Ah  
00A6E97D  mov         eax,dword ptr [ebp-1ACh]  
00A6E983  push        eax  
00A6E984  lea         ecx,[ebp-188h]  
00A6E98A  push        ecx  
00A6E98B  lea         ecx,[e]  
00A6E98E  call        Complex::operator= (0A523A6h)  
00A6E993  lea         ecx,[ebp-188h]  
00A6E999  call        Complex::~Complex (0A5183Eh)  
00A6E99E  mov         byte ptr [ebp-4],9  
00A6E9A2  lea         ecx,[ebp-170h]  
00A6E9A8  call        Complex::~Complex (0A5183Eh)  
00A6E9AD  mov         byte ptr [ebp-4],8  
00A6E9B1  lea         ecx,[ebp-158h]  
00A6E9B7  call        Complex::~Complex (0A5183Eh)  
00A6E9BC  mov         byte ptr [ebp-4],7  
00A6E9C0  lea         ecx,[ebp-140h]  
00A6E9C6  call        Complex::~Complex (0A5183Eh)  
00A6E9CB  mov         byte ptr [ebp-4],6  
00A6E9CF  lea         ecx,[ebp-128h]  
00A6E9D5  call        Complex::~Complex (0A5183Eh) 

其实从长度来看就知道它效率较低,重点看 0x005755A5 位置,依然是 return *this 的位置。这里它做了两件事,首先类似的,获取到了 [this],并赋值给了 eax,然后对 eax 进行压栈,注意这里压的不是主函数的栈,而是operator=的栈(和上面的压栈不同)。然后它 Call 了Complex 的构造函数,并构造了一个新的Complex类,存到了 [ebp-48h] 的位置(没学过汇编,不太确定)。在主函数部分,重点看 0x00A6E909-0x00A6E925 这一部分,虽然我没学过汇编,但我感觉它是经历了两次寄存器寻址过程才真正找到上面创建的临时Complex,然后压到主函数的栈中!

既然每运行一次 operator= 就创建一个 Complex,那么肯定要析构,就在 0x00A6E99E~0x00A6E9D5 部分,因此它的效率肯定是很低的。

这里引出了一个新问题,既然我们返回 Complex 的时候最好返回 Complex& ,那么我们在返回 int 的时候,为什么不去直接返回 int& 呢?
https://www.cnblogs.com/kekec/archive/2013/02/16/2913607.html 这个博客给了我答案!里面有一张图:
图片描述

这幅图是函数的栈存储结构,重点看右边方框的字:也就是说,只有是复合类型的时候(这里的 Complex就是复合类型),它才会构造中间对象;而如果是基本类型or引用,就会直接通过 eax 返回给上层函数!因此我们在这里才提出需要返回 Complex&,而在之前写 int 函数不用考虑这个问题!

由于我没有学过汇编,因此不保证说的一定是对的,仅给大家作为一个参考!

以上是个人的一些简单见解,恳请各位同学及老师的指正!

写回答

1回答

quickzhao

2023-12-26

如果你盯着C++编译器的行为,可能会让你惊讶。因为C++11之前和之后的版本,对于返回值的优化处理会有很大的差别。早期的C++中如果能返回引用的场景尽量返回引用,而如今的C++编译器对返回值的优化做了很多优化,由于移动语义等的出现,以后的趋势可能会趋向于直接返回对象。但是这里的赋值运算符=又不一样,因为C++中对于赋值运算符有着设计上的原则,比如需要满足链式赋值,如a=b=c; 并且常规的操作已经确定了要返回引用,如果随便更改其行为是会操作使用上的不一致,有违C++重载的原则。C++中有些原则是约定俗成的,必须遵守;而且还要关注C++标准未来的发展趋势,才能准确的把握其精髓。

0
0

重学C++ ,重构你的C++知识体系

一部大片,一段历史,构建C++知识框架的同时重塑你的编程思维

3884 学习 · 1103 问题

查看课程