总所周知,函数体返回值那里没有&,就会返回一个中间变量。
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 2019-07-14 17:36:33 +08:00
首先,不能这样写。你让变量活在哪里?
至于楼主的问题, 对向返回的是一个地址. 最后到 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 1
@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 嗯嗯,互相学习 :)
|