关于重载=运算符中返回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++标准未来的发展趋势,才能准确的把握其精髓。
00
相似问题
回答 2
回答 1