积极答复者
cout出现变量延迟解析,这是bug还是语言特性?

问题
-
int * getInt2(int a[]) { (*a) *= 2; return a; } int main(int argc,char *argv[]) { int intInit = 1; int * (*intPtr)(int *) = getInt2; cout << intInit++ << ":" << intInit << endl; cout << intPtr(&intInit) << ":" << intInit<<endl; cout << intInit << endl; cout << *intPtr(&intInit) << endl; cout << (*intPtr)(&intInit) << ":" << *(*intPtr)(&intInit) << endl; cout << intInit << endl; }
实际运行结果:
1:2 012FFABC:2 4 8 012FFABC:16 32
而期望结果是:
1:2 012FFABC:4 4 8 012FFABC:32 32
这个是我哪里理解有问题还是确实它出现bug了?感觉好像是bug。
使用的版本是:
visual studio 15.9.9
.net framework 4.7.03190- 已编辑 木叶清风谢 2019年4月17日 9:43 添加环境
答案
-
为了简单起见,直接分析汇编码吧。
int intInit = 1; 00E126B2 mov dword ptr [intInit],1 int * (*intPtr)(int *) = getInt2; 00E126B9 mov dword ptr [intPtr],offset getInt2 (0E11262h) cout << intInit++ << ":" << intInit << endl; 00E126C0 mov eax,dword ptr [intInit] 00E126C3 mov dword ptr [ebp-0E0h],eax 00E126C9 mov ecx,dword ptr [intInit] 00E126CC add ecx,1 00E126CF mov dword ptr [intInit],ecx 00E126D2 mov esi,esp 00E126D4 push offset std::endl<char,std::char_traits<char> > (0E112ADh) 00E126D9 mov edi,esp 00E126DB mov edx,dword ptr [intInit] 00E126DE push edx 00E126DF push offset string ":" (0E19B30h) 00E126E4 mov ebx,esp 00E126E6 mov eax,dword ptr [ebp-0E0h] 00E126EC push eax 00E126ED mov ecx,dword ptr [_imp_?cout@std@@3V?$basic_ostream@DU?$char_traits@D@std@@@1@A (0E1D0DCh)] 00E126F3 call dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (0E1D0A4h)] 00E126F9 cmp ebx,esp 00E126FB call __RTC_CheckEsp (0E1128Ah) 00E12700 push eax 00E12701 call std::operator<<<std::char_traits<char> > (0E1120Dh) 00E12706 add esp,8 00E12709 mov ecx,eax 00E1270B call dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (0E1D0A4h)] 00E12711 cmp edi,esp 00E12713 call __RTC_CheckEsp (0E1128Ah) 00E12718 mov ecx,eax 00E1271A call dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (0E1D0A8h)] 00E12720 cmp esi,esp 00E12722 call __RTC_CheckEsp (0E1128Ah) cout << intPtr(&intInit) << ":" << intInit << endl; 00E12727 mov esi,esp 00E12729 push offset std::endl<char,std::char_traits<char> > (0E112ADh) 00E1272E mov edi,esp 00E12730 mov eax,dword ptr [intInit] 00E12733 push eax 00E12734 push offset string ":" (0E19B30h) 00E12739 mov ebx,esp 00E1273B lea ecx,[intInit] 00E1273E push ecx 00E1273F call dword ptr [intPtr] 00E12742 add esp,4 00E12745 cmp ebx,esp 00E12747 call __RTC_CheckEsp (0E1128Ah) 00E1274C mov ebx,esp 00E1274E push eax 00E1274F mov ecx,dword ptr [_imp_?cout@std@@3V?$basic_ostream@DU?$char_traits@D@std@@@1@A (0E1D0DCh)] 00E12755 call dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (0E1D0A0h)] 00E1275B cmp ebx,esp 00E1275D call __RTC_CheckEsp (0E1128Ah) 00E12762 push eax 00E12763 call std::operator<<<std::char_traits<char> > (0E1120Dh) 00E12768 add esp,8 00E1276B mov ecx,eax 00E1276D call dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (0E1D0A4h)] 00E12773 cmp edi,esp 00E12775 call __RTC_CheckEsp (0E1128Ah) 00E1277A mov ecx,eax 00E1277C call dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (0E1D0A8h)] 00E12782 cmp esi,esp 00E12784 call __RTC_CheckEsp (0E1128Ah)
cout << intInit++ << ":" << intInit << endl;和cout << intPtr(&intInit) << ":" << intInit << endl;这两句后面的输出流部分是一样的,所以咱们就看前面计算和压栈的这部分。
cout << intInit++ << ":" << intInit << endl; 00E126C0 mov eax,dword ptr [intInit] 00E126C3 mov dword ptr [ebp-0E0h],eax 00E126C9 mov ecx,dword ptr [intInit] 00E126CC add ecx,1 00E126CF mov dword ptr [intInit],ecx 00E126D2 mov esi,esp 00E126D4 push offset std::endl<char,std::char_traits<char> > (0E112ADh) 00E126D9 mov edi,esp 00E126DB mov edx,dword ptr [intInit] 00E126DE push edx 00E126DF push offset string ":" (0E19B30h) 00E126E4 mov ebx,esp 00E126E6 mov eax,dword ptr [ebp-0E0h] cout << intPtr(&intInit) << ":" << intInit << endl; 00E12727 mov esi,esp 00E12729 push offset std::endl<char,std::char_traits<char> > (0E112ADh) 00E1272E mov edi,esp 00E12730 mov eax,dword ptr [intInit] 00E12733 push eax 00E12734 push offset string ":" (0E19B30h) 00E12739 mov ebx,esp 00E1273B lea ecx,[intInit] 00E1273E push ecx 00E1273F call dword ptr [intPtr] 00E12742 add esp,4 00E12745 cmp ebx,esp 00E12747 call __RTC_CheckEsp (0E1128Ah) 00E1274C mov ebx,esp
很显然在经过编译器优化之后,实际步骤和理论上的步骤不尽相同。cout << intInit++ << ":" << intInit << endl;这一部分是:
mov eax,dword ptr [intInit]
mov dword ptr [ebp-0E0h],eax //这一句和上一句是将变量intInit赋值给匿名变量ebp-0E0h
mov ecx,dword ptr [intInit] //变量intInit存入寄存器ecx
add ecx,1 //等于是++intInit
mov dword ptr [intInit],ecx //ecx中的计算结果返回给变量intInit
mov esi,esp
push offset std::endl<char,std::char_traits<char> > (0E112ADh)
mov edi,esp
mov edx,dword ptr [intInit] //将经过++后的intInit变量压栈
push edx
push offset string ":" (0E19B30h)
mov ebx,esp
mov eax,dword ptr [ebp-0E0h] //将匿名变量ebp-0E0h压栈因此上面的代码相当于:
auto temp = intInit; ++intInit; cout << temp << ":" << intInit << endl;
这个和之前的理论预测完全相同。
接下来再看cout << intPtr(&intInit) << ":" << intInit << endl;这部分。
mov esi,esp
push offset std::endl<char,std::char_traits<char> > (0E112ADh) //直接开始压栈
mov edi,esp
mov eax,dword ptr [intInit] //将intInit变量压栈
push eax
push offset string ":" (0E19B30h)
mov ebx,esp
lea ecx,[intInit]
push ecx //将intInit变量的地址压栈
call dword ptr [intPtr] //通过函数指针调函数
add esp,4
cmp ebx,esp
call __RTC_CheckEsp (0E1128Ah)
mov ebx,esp这段代码相当于:
cout << ":" << intInit << endl; intPtr(&intInit); cout << &intInit;
为什么会这样呢?因为函数的返回值是一个值,不是计算语句,因此cout << intPtr(&intInit) << ":" << intInit << endl;这句话中实际上没有任何计算,所以直接就开始压栈了,没有计算过程,遇到了函数的时候就call一下,反正函数栈对当前栈没影响。如果你把前面cout << intPtr(&intInit) << ":" << intInit << endl;这句改换成cout << ++(*intPtr(&intInit)) << ":" << intInit << endl;再运行就会得到5:5了。所以结论就是函数是一个指针,唯一需要的指令仅仅是call,不生成负责计算的汇编指令,因此也就没有计算过程,只有实际用到汇编计算指令时才需要“先计算”过程。
- 已标记为答案 木叶清风谢 2019年4月19日 10:13
全部回复
-
是先完成计算,再压栈,最后出栈显示。cout是后进先出结构。
例如:cout << i++ << i << ++i;
可以理解为:
++i;
auto temp = i;
++i;
cout << temp << i << i;
因为后置的自增减运算符是有临时变量的。
i++;这一句相当于下面这两行:
auto temp = i;
++i;
其实这个过程和函数实参的计算与压栈过程是完全一样的,先计算后压栈,从右向左进行。当然这个不在c/c++标准里,不同编译器的实现允许不同,因此c/c++程序员一般都遵从不在函数实参或cout<<这样的操作中进行有相互影响的运算这一准则。比如:
int add(int a, int b, int c)
{
return a + b + c;
}
auto i = 0;
调用add(i++, i, ++i);后得到的是5。- 已建议为答案 Jeanine ZhangMicrosoft contingent staff, Moderator 2019年4月18日 1:47
- 已标记为答案 木叶清风谢 2019年4月18日 22:42
- 取消答案标记 木叶清风谢 2019年4月18日 23:07
-
我又屡了一下思路,发现还是有些不对,如果是先计算再压栈的话,可以理解第一行为什么会输出是1:2,可是如果我把第二行改成:
cout << *intPtr(&intInit) << ":" << intInit<<endl;
那么它的输出是4:2。按照上面的思路,先计算完再压栈,应该是
intInit
*intPtr(&intInit)
计算完再压栈的话应该都是4:4啊,为啥是4:2,感觉这个通过指针获取变量内容和直接获取是不是哪里不一样?还是说就是边计算边压栈,那这样的话,第一行应该还是1:1啊。苦恼啊。。。
- 已编辑 木叶清风谢 2019年4月18日 23:05 格式问题
-
int intInit = 1;
这是在c++ 17标准运行的结果,看来确实是内部机制哪里有问题,等有时间研究下c++ 17的标准。
int * (*intPtr)(int *) = getInt2;
cout << intInit++ << ":" << intInit << endl;
cout << *intPtr(&intInit) << ":" << intInit<<endl;
cout << intInit << endl;
cout << *intPtr(&intInit) << endl;
//(* ptr)函数书写
cout << *(*intPtr)(&intInit) << ":" << *(*intPtr)(&intInit) << endl;
cout << intInit << endl;运行结果为:
1:2 4:4 4 8 16:32 32
- 已编辑 木叶清风谢 2019年4月19日 9:11 添加代码
-
为了简单起见,直接分析汇编码吧。
int intInit = 1; 00E126B2 mov dword ptr [intInit],1 int * (*intPtr)(int *) = getInt2; 00E126B9 mov dword ptr [intPtr],offset getInt2 (0E11262h) cout << intInit++ << ":" << intInit << endl; 00E126C0 mov eax,dword ptr [intInit] 00E126C3 mov dword ptr [ebp-0E0h],eax 00E126C9 mov ecx,dword ptr [intInit] 00E126CC add ecx,1 00E126CF mov dword ptr [intInit],ecx 00E126D2 mov esi,esp 00E126D4 push offset std::endl<char,std::char_traits<char> > (0E112ADh) 00E126D9 mov edi,esp 00E126DB mov edx,dword ptr [intInit] 00E126DE push edx 00E126DF push offset string ":" (0E19B30h) 00E126E4 mov ebx,esp 00E126E6 mov eax,dword ptr [ebp-0E0h] 00E126EC push eax 00E126ED mov ecx,dword ptr [_imp_?cout@std@@3V?$basic_ostream@DU?$char_traits@D@std@@@1@A (0E1D0DCh)] 00E126F3 call dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (0E1D0A4h)] 00E126F9 cmp ebx,esp 00E126FB call __RTC_CheckEsp (0E1128Ah) 00E12700 push eax 00E12701 call std::operator<<<std::char_traits<char> > (0E1120Dh) 00E12706 add esp,8 00E12709 mov ecx,eax 00E1270B call dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (0E1D0A4h)] 00E12711 cmp edi,esp 00E12713 call __RTC_CheckEsp (0E1128Ah) 00E12718 mov ecx,eax 00E1271A call dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (0E1D0A8h)] 00E12720 cmp esi,esp 00E12722 call __RTC_CheckEsp (0E1128Ah) cout << intPtr(&intInit) << ":" << intInit << endl; 00E12727 mov esi,esp 00E12729 push offset std::endl<char,std::char_traits<char> > (0E112ADh) 00E1272E mov edi,esp 00E12730 mov eax,dword ptr [intInit] 00E12733 push eax 00E12734 push offset string ":" (0E19B30h) 00E12739 mov ebx,esp 00E1273B lea ecx,[intInit] 00E1273E push ecx 00E1273F call dword ptr [intPtr] 00E12742 add esp,4 00E12745 cmp ebx,esp 00E12747 call __RTC_CheckEsp (0E1128Ah) 00E1274C mov ebx,esp 00E1274E push eax 00E1274F mov ecx,dword ptr [_imp_?cout@std@@3V?$basic_ostream@DU?$char_traits@D@std@@@1@A (0E1D0DCh)] 00E12755 call dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (0E1D0A0h)] 00E1275B cmp ebx,esp 00E1275D call __RTC_CheckEsp (0E1128Ah) 00E12762 push eax 00E12763 call std::operator<<<std::char_traits<char> > (0E1120Dh) 00E12768 add esp,8 00E1276B mov ecx,eax 00E1276D call dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (0E1D0A4h)] 00E12773 cmp edi,esp 00E12775 call __RTC_CheckEsp (0E1128Ah) 00E1277A mov ecx,eax 00E1277C call dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (0E1D0A8h)] 00E12782 cmp esi,esp 00E12784 call __RTC_CheckEsp (0E1128Ah)
cout << intInit++ << ":" << intInit << endl;和cout << intPtr(&intInit) << ":" << intInit << endl;这两句后面的输出流部分是一样的,所以咱们就看前面计算和压栈的这部分。
cout << intInit++ << ":" << intInit << endl; 00E126C0 mov eax,dword ptr [intInit] 00E126C3 mov dword ptr [ebp-0E0h],eax 00E126C9 mov ecx,dword ptr [intInit] 00E126CC add ecx,1 00E126CF mov dword ptr [intInit],ecx 00E126D2 mov esi,esp 00E126D4 push offset std::endl<char,std::char_traits<char> > (0E112ADh) 00E126D9 mov edi,esp 00E126DB mov edx,dword ptr [intInit] 00E126DE push edx 00E126DF push offset string ":" (0E19B30h) 00E126E4 mov ebx,esp 00E126E6 mov eax,dword ptr [ebp-0E0h] cout << intPtr(&intInit) << ":" << intInit << endl; 00E12727 mov esi,esp 00E12729 push offset std::endl<char,std::char_traits<char> > (0E112ADh) 00E1272E mov edi,esp 00E12730 mov eax,dword ptr [intInit] 00E12733 push eax 00E12734 push offset string ":" (0E19B30h) 00E12739 mov ebx,esp 00E1273B lea ecx,[intInit] 00E1273E push ecx 00E1273F call dword ptr [intPtr] 00E12742 add esp,4 00E12745 cmp ebx,esp 00E12747 call __RTC_CheckEsp (0E1128Ah) 00E1274C mov ebx,esp
很显然在经过编译器优化之后,实际步骤和理论上的步骤不尽相同。cout << intInit++ << ":" << intInit << endl;这一部分是:
mov eax,dword ptr [intInit]
mov dword ptr [ebp-0E0h],eax //这一句和上一句是将变量intInit赋值给匿名变量ebp-0E0h
mov ecx,dword ptr [intInit] //变量intInit存入寄存器ecx
add ecx,1 //等于是++intInit
mov dword ptr [intInit],ecx //ecx中的计算结果返回给变量intInit
mov esi,esp
push offset std::endl<char,std::char_traits<char> > (0E112ADh)
mov edi,esp
mov edx,dword ptr [intInit] //将经过++后的intInit变量压栈
push edx
push offset string ":" (0E19B30h)
mov ebx,esp
mov eax,dword ptr [ebp-0E0h] //将匿名变量ebp-0E0h压栈因此上面的代码相当于:
auto temp = intInit; ++intInit; cout << temp << ":" << intInit << endl;
这个和之前的理论预测完全相同。
接下来再看cout << intPtr(&intInit) << ":" << intInit << endl;这部分。
mov esi,esp
push offset std::endl<char,std::char_traits<char> > (0E112ADh) //直接开始压栈
mov edi,esp
mov eax,dword ptr [intInit] //将intInit变量压栈
push eax
push offset string ":" (0E19B30h)
mov ebx,esp
lea ecx,[intInit]
push ecx //将intInit变量的地址压栈
call dword ptr [intPtr] //通过函数指针调函数
add esp,4
cmp ebx,esp
call __RTC_CheckEsp (0E1128Ah)
mov ebx,esp这段代码相当于:
cout << ":" << intInit << endl; intPtr(&intInit); cout << &intInit;
为什么会这样呢?因为函数的返回值是一个值,不是计算语句,因此cout << intPtr(&intInit) << ":" << intInit << endl;这句话中实际上没有任何计算,所以直接就开始压栈了,没有计算过程,遇到了函数的时候就call一下,反正函数栈对当前栈没影响。如果你把前面cout << intPtr(&intInit) << ":" << intInit << endl;这句改换成cout << ++(*intPtr(&intInit)) << ":" << intInit << endl;再运行就会得到5:5了。所以结论就是函数是一个指针,唯一需要的指令仅仅是call,不生成负责计算的汇编指令,因此也就没有计算过程,只有实际用到汇编计算指令时才需要“先计算”过程。
- 已标记为答案 木叶清风谢 2019年4月19日 10:13
-
总算把前面的问题整理完了,结论就是上面的最终回复。现在再来看看c++17:
cout << intInit++ << ":" << intInit << endl; 003F22DC mov dword ptr [ebp-0E4h],eax 003F22E2 mov eax,dword ptr [intInit] 003F22E5 mov dword ptr [ebp-0E8h],eax 003F22EB mov esi,esp 003F22ED mov ecx,dword ptr [ebp-0E8h] 003F22F3 push ecx 003F22F4 mov ecx,dword ptr [ebp-0E4h] 003F22FA call dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (03FD0A4h)] 003F2300 cmp esi,esp 003F2302 call __RTC_CheckEsp (03F128Ah) 003F2307 mov dword ptr [ebp-0ECh],eax 003F230D mov esi,esp 003F230F push offset std::endl<char,std::char_traits<char> > (03F12ADh) 003F2314 mov ecx,dword ptr [ebp-0ECh] 003F231A call dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (03FD0A8h)] 003F2320 cmp esi,esp 003F2322 call __RTC_CheckEsp (03F128Ah) cout << *intPtr(&intInit) << ":" << intInit << endl; 003F2327 mov eax,dword ptr [intPtr] 003F232A mov dword ptr [ebp-0E0h],eax 003F2330 mov esi,esp 003F2332 lea ecx,[intInit] 003F2335 push ecx 003F2336 call dword ptr [ebp-0E0h] 003F233C add esp,4 003F233F cmp esi,esp 003F2341 call __RTC_CheckEsp (03F128Ah) 003F2346 mov edx,dword ptr [eax] 003F2348 mov dword ptr [ebp-0E4h],edx 003F234E mov esi,esp 003F2350 mov eax,dword ptr [ebp-0E4h] 003F2356 push eax 003F2357 mov ecx,dword ptr [_imp_?cout@std@@3V?$basic_ostream@DU?$char_traits@D@std@@@1@A (03FD0DCh)] 003F235D call dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (03FD0A4h)] 003F2363 cmp esi,esp 003F2365 call __RTC_CheckEsp (03F128Ah) 003F236A push offset string ":" (03F9B30h) 003F236F push eax 003F2370 call std::operator<<<std::char_traits<char> > (03F120Dh) 003F2375 add esp,8 003F2378 mov dword ptr [ebp-0E8h],eax 003F237E mov ecx,dword ptr [intInit] 003F2381 mov dword ptr [ebp-0ECh],ecx 003F2387 mov esi,esp 003F2389 mov edx,dword ptr [ebp-0ECh] 003F238F push edx 003F2390 mov ecx,dword ptr [ebp-0E8h] 003F2396 call dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (03FD0A4h)] 003F239C cmp esi,esp 003F239E call __RTC_CheckEsp (03F128Ah) 003F23A3 mov dword ptr [ebp-0F0h],eax 003F23A9 mov esi,esp 003F23AB push offset std::endl<char,std::char_traits<char> > (03F12ADh) 003F23B0 mov ecx,dword ptr [ebp-0F0h] 003F23B6 call dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (03FD0A8h)] 003F23BC cmp esi,esp 003F23BE call __RTC_CheckEsp (03F128Ah)
很显然c++17中call函数的这部分被挪到前面去了。
-
看看release的汇编码吧。
这个更清楚。c++14里面是先计算,然后依次压栈,然后call函数,最后再塞到stream里,这几步依次进行。c++17是先计算并call函数,然后直接往stream里面塞,每压一个数据就紧接着call一次<<运算符函数。这个和c++标准显然无关,c++标准里面没有规定编译器行为,显然是微软自己调整了编译顺序。//c++14:cout << *intPtr(&intInit) << ":" << intInit << endl; 00F8108D push dword ptr [esp+4] 00F81091 lea eax,[esp+8] 00F81095 push eax 00F81096 call getInt2 (0F81030h) 00F8109B mov ecx,dword ptr [_imp_?cout@std@@3V?$basic_ostream@DU?$char_traits@D@std@@@1@A (0F8307Ch)] 00F810A1 add esp,4 00F810A4 push eax 00F810A5 call dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (0F83034h)] 00F810AB mov ecx,eax 00F810AD call std::operator<<<std::char_traits<char> > (0F81330h) 00F810B2 mov ecx,eax 00F810B4 call dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (0F8303Ch)] 00F810BA mov ecx,eax 00F810BC call dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (0F83048h)]
//c++17:cout << *intPtr(&intInit) << ":" << intInit << endl; 01191090 call getInt2 (01191030h) 01191095 mov ecx,dword ptr [_imp_?cout@std@@3V?$basic_ostream@DU?$char_traits@D@std@@@1@A (0119307Ch)] 0119109B add esp,4 0119109E push eax 0119109F call dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (01193034h)] 011910A5 mov ecx,eax 011910A7 call std::operator<<<std::char_traits<char> > (01191330h) 011910AC push dword ptr [esp+8] 011910B0 mov ecx,eax 011910B2 call dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (0119303Ch)] 011910B8 push offset std::endl<char,std::char_traits<char> > (01191550h) 011910BD mov ecx,eax 011910BF call dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (01193048h)]