
总所周知,函数体返回值那里没有&,就会返回一个中间变量。
class sale { public: int i = 1; }; sale add(const sale& lift, const sale& right) { sale sum = lift; sum.i += right.i; return sum; } int main() { sale one; sale two; const sale& global = add(one, two); } 假设有如上代码,进行反汇编后,汇编如下:
const sale& global = add(one, two); 00D51A32 8D 45 E8 lea eax,[two] 00D51A35 50 push eax 00D51A36 8D 4D F4 lea ecx,[one] 00D51A39 51 push ecx 00D51A3A E8 40 F9 FF FF call add (0D5137Fh) 00D51A3F 83 C4 08 add esp,8 #调用完毕后,清除栈空间 00D51A42 89 85 04 FF FF FF mov dword ptr [ebp-0FCh],eax #为中间变量分配空间 00D51A48 8B 95 04 FF FF FF mov edx,dword ptr [ebp-0FCh] 00D51A4E 89 55 D0 mov dword ptr [ebp-30h],edx #把中间变量复制给新的变量 00D51A51 8D 45 D0 lea eax,[ebp-30h] #导入新变量的地址 00D51A54 89 45 DC mov dword ptr [global],eax #把这个地址给 global,因为引用本质是指针 上面写的注释不一定对,但为什么这里要分配两次空间呢? 如果我换一个简单的程序:
int re() { return 5; } int main() { //int a = 1; const int& b = re(); } 他的汇编就和我想象中一样了,只分配一次空间:
const int& b = re(); 00B019B2 E8 0E FA FF FF call re (0B013C5h) 00B019B7 89 45 E8 mov dword ptr [ebp-18h],eax #分配空间 00B019BA 8D 45 E8 lea eax,[ebp-18h] #把地址导入 eax 00B019BD 89 45 F4 mov dword ptr [b],eax #把 eax 赋值给 b,因为 b 是引用,相当于指针 1 lhx2008 2019-07-14 17:15:02 +08:00 我猜,关掉编译器优化,反汇编之后结果就一样了 |
2 ipwx 2019-07-14 17:23:04 +08:00 首先,楼主你两段代码都是不合法的。 对于不合法的代码,C++ 编译器没有义务给你吐出合理的结果。 |
3 stephen9357 2019-07-14 17:23:24 +08:00 debug 编译的代码,编译器怎么顺手怎么来,别当真。甚至用 IDA 看系统自带的 release 动态库时,也遇到过类似情况,编译器毕竟不能保证每一行代码编译后都是最优的,能看明白就可以了。 |
4 amiwrong123 OP @lhx2008 用得是 vs2017,找了找,在当前项目的什么设置里面,找到了“优化”,里面有什么内联函数拓展、启动内部函数什么的,但基本都是关着的。 @ipwx const int& b = re();原来这种用法是不合法的吗?有点没懂啊,我知道如果返回局部变量的引用,这种情况是不合法的,虽然编译器只是报个 warning。 @stephen9357 原来是这样的啊。确实大概能看明白,比如函数体返回值那里有没有&(返回的是不是引用),会体现到汇编上去。虽然分配了两次空间,但可能就是编译器没优化好呗。 |
5 thedrwu 首先,不能这样写。你让变量活在哪里? 至于楼主的问题, 对向返回的是一个地址. 最后到 edx 里的是地址的地址,即 %ebp-30h 这个数字。中间变量没毛病。 至于第二段, 不是地址的地址, 而只有一层地址。 没仔细看,仅供参考 |
6 akira 2019-07-14 17:47:02 +08:00 第一个是类吧 类应该是需要 2 个指针来表达的 |
7 hoyixi 2019-07-14 17:49:56 +08:00 'lift' and right |
8 amiwrong123 OP @thedrwu 好吧,首先是不是,引用绑定到返回的中间变量,这种写法就是错的吗 然后,我又改了一下,改成 sale global = add(one, two);汇编就变成了: 00B019D2 8D 45 E8 lea eax,[two] 00B019D5 50 push eax 00B019D6 8D 4D F4 lea ecx,[one] 00B019D9 51 push ecx 00B019DA E8 F0 F9 FF FF call add (0B013CFh) 00B019DF 83 C4 08 add esp,8 00B019E2 89 85 10 FF FF FF mov dword ptr [ebp-0F0h],eax 00B019E8 8B 95 10 FF FF FF mov edx,dword ptr [ebp-0F0h] 00B019EE 89 55 DC mov dword ptr [global],edx 好像跟是不是对象没关系,只用一个地址就好了。 哎,我是不是有点钻牛角尖了,但是又有点好奇。 @akira 你看上面的汇编,好像跟是不是对象没关系啊。 @hoyixi 哈哈哈,一时手滑啦 |
9 aliwalker 2019-07-14 18:15:24 +08:00 我用 clang 编译了一下第一段,发现没有写把 add 返回值写两次内存的操作... 100000f5a: 48 8d 7d f8 leaq -8(%rbp), %rdi # &one 100000f5e: 48 8d 75 f0 leaq -16(%rbp), %rsi # &two 100000f62: e8 a9 ff ff ff callq -87 <__Z3addRK4saleS1_> # call add 100000f67: 31 c9 xorl %ecx, %ecx # 清零 100000f69: 89 45 e0 movl %eax, -32(%rbp) # 返回值存到临时变量 100000f6c: 48 8d 75 e0 leaq -32(%rbp), %rsi # 指针 100000f70: 48 89 75 e8 movq %rsi, -24(%rbp) # 指针值存到 global 100000f74: 89 c8 movl %ecx, %eax # 返回值为 0 100000f76: 48 83 c4 20 addq $32, %rsp 100000f7a: 5d popq %rbp 100000f7b: c3 retq 用 const 引用返回值是可以的,这个临时变量在 call site 的 frame 上是有分配空间的。如果改成 sale &global = add(one, two);就不行了:initial value of reference to non-const must be an lvalue。 第二段结果是一样的,只是生成的是 x64 机器码。 |
10 aliwalker 2019-07-14 18:21:56 +08:00 补充一下,从第二段反汇编出来的内容可以看到为什么不是 const 引用不行: _main: 100000f90: 55 pushq %rbp 100000f91: 48 89 e5 movq %rsp, %rbp 100000f94: 48 83 ec 10 subq $16, %rsp 100000f98: e8 e3 ff ff ff callq -29 <__Z2rev> 100000f9d: 31 c9 xorl %ecx, %ecx 100000f9f: 89 45 f4 movl %eax, -12(%rbp) 100000fa2: 48 8d 55 f4 leaq -12(%rbp), %rdx 100000fa6: 48 89 55 f8 movq %rdx, -8(%rbp) 100000faa: 89 c8 movl %ecx, %eax 100000fac: 48 83 c4 10 addq $16, %rsp 100000fb0: 5d popq %rbp 100000fb1: c3 retq 返回的 int 是 4bytes,写在-12(%rbp)上,但是指针 b 的位置-8(%rbp)其实和这个返回的 temp 值重合。 |
11 zjsxwc 2019-07-14 18:33:46 +08:00 via Android 难道没人和我一样奇怪 sum 在栈里的内容在函数结束时会不会被释放吗 |
12 ipwx 2019-07-14 18:43:05 +08:00 @amiwrong123 不合法的理由,见 5L 和 11L 的疑惑。 |
13 ipwx 2019-07-14 18:45:31 +08:00 另外我大概理解楼主为什么要写不合法代码的理由了,是想研究 C++ 返回值地址的问题嘛? 但是 C++ 编译器会根据返回值的赋值进行代码优化的。 比如: class A { ... }; A someFunction() { A a; return a; } A target = someFunction(); 在深度优化的时候不会发生拷贝,直接在返回值 target 上调用构造函数。 |
14 zjsxwc 2019-07-14 18:46:42 +08:00 via Android In C++, unlike in C#, struct makes few differences with class. A struct is a class whose default visibility is public. Whether the allocation is performed on the stack or in the heap depends on the way you allocate your instance class A; void f() { A a;//stack allocated A *a1 = new A();// heap } |
16 aliwalker 2019-07-14 18:50:15 +08:00 via Android @ipwx yep. Return value optimization. 是 copy elision 的一种 |
17 lcdtyph 2019-07-14 20:27:46 +08:00 @ipwx #12 @thedrwu #5 两个都是合法的,临时变量在绑定到常引用之后生命周期会被延长,参见 https://en.cppreference.com/w/cpp/language/lifetime 在 c++11 右值出现之前都是这样做的。 |
18 amiwrong123 OP @aliwalker 既然你用 clang 编译没有出现两次写内存,那可能我的编译器的问题吧。 然后你的第二段,我看了,它把返回值 int 存在了-12,-11,-10,-9 这四个字节里,然后把地址存在了-8,-7,...,-1,没有什么重合啊感觉。所以没理解,“为什么不是 const 引用不行”。 |
19 amiwrong123 OP |
20 amiwrong123 OP @zjsxwc 你们都说,内存会被释放掉。那么我打印 global 的值的时候,肯定就是非法值呗。 class sale { public: int i = 1; }; sale add(const sale& lift, const sale& right) { sale sum = lift; sum.i += right.i; return sum; } int main() { sale one; sale two; const sale& global = add(one, two); cout << global.i; } 执行这个代码,我发现还是能打印出来 2 啊,也不是什么非法值。 |
21 nethard 2019-07-14 21:20:09 +08:00 via iPhone 哈哈,楼主要是这么喜欢这样写,返回一个栈上的指针,可以去写 go 玩玩。 |
22 lcdtyph 2019-07-14 21:25:17 +08:00 @amiwrong123 #18 clang 有个参数 -fno-elide-constructors 可以让编译器不做 rvo,这样在你第一种代码里一定会分配两次内存。默认是会做 rvo 的。 visual studio 的编译器不知道有没有类似的选项…… |
23 ispinfx 2019-07-14 21:27:50 +08:00 via iPhone 好奇那么多说不合法的…这不是常引用最常见的用法吗 |
24 lcdtyph 2019-07-14 21:32:57 +08:00 还有实际上 lz 的情况在 c++17 中一定不会产生临时对象,这是 c++17 新的 guaranteed copy elision 特性来保证的。 |
25 amiwrong123 OP @lcdtyph 看到啦,在 Temporary object lifetime 的 There are two exceptions from that:,我这个属于 const lvalue reference。 你说的-fno-elide-constructors,这又是我的知识盲区了。。。看了看博客,意思就是会省略一些中间变量的创建,因为两次拷贝和一次拷贝效果是一样的。但为啥你说,我第一种代码里一定会分配两次内存,不是默认是会优化的吗? vs2017 里面好像没有这个选项。。 guaranteed copy elision,又是一个知识点,我拿小本本记上。 |
26 amiwrong123 OP |
27 lcdtyph 2019-07-14 22:02:31 +08:00 via iPhone @amiwrong123 这个选项是强制在可以省略临时对象的情况下调用拷贝构造,所以打开这个选项就相当于没了返回值优化,就会造成一次额外的拷贝 |
28 ispinfx 2019-07-14 22:36:11 +08:00 via iPhone @amiwrong123 这个我记得以前上谭 cpp primer 里就有说的吧,虽然我已不写 cpp10 年了。。 |
29 karia 2019-07-14 22:39:29 +08:00 via Android 《程序员的自我修养》内存那一章(11 章?忘了)讲到过这个问题 return 栈上大型结构的时候,caller 的 stack frame 里会分配 2 个临时空间,一个隐式的用来保存返回值,返回之后再 memcpy 给你的显式声明的 global 另外提醒楼主...乐于尝试是不错,但是不要钻牛角尖了...孔夫子曰过思而不学则殆...多看书,或许很多问题别人已经研究过了 |
30 lrxiao 2019-07-15 07:39:35 +08:00 在 debug 模式看汇编 yy 分配空间... 也可能只是中间变量没优化而已.. 你需要的是写个 destructor 然后看调用次数.. |
31 lrxiao 2019-07-15 07:45:49 +08:00 这两个都是 copy elision/rvo 后的结果了 并没有额外的 constructor/destructor 介入 |
32 14m3 2019-07-15 09:25:58 +08:00 1. 楼主,以后可以在 https://godbolt.org/(一个网页版的交互式编译器)上面来测试,可以选择不同的编译器,开启不同的编译选项,也能很方便看到汇编代码 2. 前面 @lcdtyph 已经说过了,const lvalue reference 会延长临时对象的生命周期,所以问题中的代码是合法的 3. 其实讨论这个代码,是需要确定讨论的环境的,是 C++11/14,还是 C++17。因为 C++17 标准中添加了 Guaranteed Copy Elision,重新定义了 value category 中 prvalues 的语义 4. 我自己测试了一下 https://godbolt.org/z/NRKI-R,在 C++17 标准下,不管 main 函数中是 const sale& global = add(one, two); 还是 const sale global = add(one, two); 问题中的代码都是调用了三次构造函数,前两次构造函数是 sale one; 和 sale two;,第三次构造函数是拷贝构造。在其他标准下,楼主可以自己测试. |
33 amiwrong123 OP |
34 amiwrong123 OP |
35 amiwrong123 OP @14m3 哇,你这个链接是个神器,回头好好研究下。确实,这个代码怎么优化的跟环境有关系啊。你这个代码而且写得很清楚,加了构造和析构的打印后,思路瞬间清晰了。 |
36 14m3 2019-07-15 20:25:05 +08:00 @amiwrong123 嗯嗯,互相学习 :) |