一、printf参数包含 i++ 和 ++i 时的反常结果
在 Visual Studio 中输入下列程序:
int main()
{
int i = 3;
printf("%d %d %d", i, i++, ++i); //输出
printf("\n%d\n", i);
return 0;
}
该程序的输出结果应该是什么呢?
首先,对于i++
和++i
,我们有如下理解:
++i
的效果等同于i = i + 1
,也就是立即将i
的结果自增1
并写回给i
;i++
的效果等同于int tmp = i , i = i + 1
,也就说本次语句执行时仍然使用i
的旧值tmp
,执行完毕后i
自增1
写回给i
;
按照以上逻辑,断言该代码输出的结果应该是:
>>3,3,5
>>5
但结果很奇怪,在VS中输出结果如下
>>5,4,5
>>5
第二行符合预期,但第一行和预期大相径庭。要搞明白为什么会这样,首先明白一件事:
printf
函数对参数的处理是从右至左的,也就是说虽然输出时的顺序为i
,i++
,++i
,但处理的顺序却是++i >> i++ >> i
;
可以简单地用一个程序证明:
int main()
{
int i = 0;
int j = 0;
printf("%d %d", j += i, i = 10);
}
输出结果:
然后需要明白另一件事:
printf
中对变量i
不是立即写回,而是等到所有参数按照从右到左的顺序依次计算完毕后统一写回的,如果其它参数中改变了i
的值,则写回时会对所有参数产生相同的影响。
可以通俗地理解为printf
中存在一个缓冲区,先将所有对i
的改变存在缓冲区中,最后统一写回。
重新分析结果:
int i = 3;
printf("%d %d %d", i, i++, ++i); //输出
>>>5,4,5
从右往左处理参数:
-
首先
++i
会将i
的值由3
改变为4
,但并不会立即写回;结果:_,_,_
-
然后是
i++
,后缀自增是比较特殊的情况,它会创建一个副本int tmp = i = 4
,实际传给printf
的是tmp
而非i
,所以在最后统一写回i
对i++
不会产生影响(下边会再谈到);结果:_,4,_
-
接着是
i
,因为前边i
自增了两次,所以i
的值此时为5
,且所有参数处理完毕,所以同时写回两个5
;结果:5,4,5
单给出结论好像不太能让人信服,所以来看看汇编代码就一目了然了(别担心,有注释的)。
以下代码来自Visual Studio 2019反汇编结果:
`int i = 3;`
00D8BBC8 mov dword ptr [i],3
`printf("%d %d %d", i, i++, ++i);`
//参数处理阶段
00D8BBCF mov eax,dword ptr [i] //取出i的结果,i->eax | i=3,eax=3
00D8BBD2 add eax,1 //实现加一,eax = eax + 1 | eax=4
00D8BBD5 mov dword ptr [i],eax //加一后写回i,eax->i | i=4 |
右1参数`++i`处理结束
00D8BBD8 mov ecx,dword ptr [i] //取出i的结果,i->ecx | i=4,ecx=4
00D8BBDB mov dword ptr [ebp-0D0h],ecx //保存副本,int tmp=i | i=4,tmp=4 |
右2参数`i++`处理结束
00D8BBE1 mov edx,dword ptr [i] //取出i的结果,i->edx | i=4,edx=4
00D8BBE4 add edx,1 //实现加一,edx = edx + 1 | edx=5
00D8BBE7 mov dword ptr [i],edx //加一后写回i,edx->i | i=5
右3参数`i`处理结束
//开始将处理好的参数压栈,让printf使用
00D8BBEA mov eax,dword ptr [i] //将右1参数`++i`取出,i->eax
00D8BBED push eax //将右1参数`++i`入栈
00D8BBEE mov ecx,dword ptr [ebp-0D0h]//将右2参数`i++`取出,tmp->ecx | tmp = 4
00D8BBF4 push ecx //将右2参数`i++`入栈
00D8BBF5 mov edx,dword ptr [i] //将右3参数`i`取出,i->edx | i = 5
00D8BBF8 push edx //将右3参数`i`入栈
00D8BBF9 push offset string "%d %d" (0D9FC18h)//将格式入栈
00D8BBFE call _printf (0D75117h)//调用函数printf
00D8BC03 add esp,10h //平衡栈
`printf("\n%d\n", i);`
00D8BC06 mov eax,dword ptr [i]
00D8BC09 push eax
00D8BC0A push offset string "\n%d\n" (0DA0000h)
00D8BC0F call _printf (0D75117h)
00D8BC14 add esp,8
`return 0;`
看完汇编代码中关于主函数对三个参数++i
,i++
,i
的准备过程,对输出5,4,5
这样的结果也就丝毫不奇怪了——
- 在Visual Studio上,
main
函数为printf
提供的三个参数总是先进行运算,最后统一按照从右到左边的顺序压到函数栈中。 - 这其中的
i
和++i
实际上是同一个内存单元中的数,这个内存单元就是变量i
的地址,因为是先运算再写回的,所以这两个参数的写回结果必然相同,因为本质上这就是对同一个变量写入两次。 - 对于参数
++i
,也是先运算再写回的,但写回的内存单元不是i
,而是一个已经保存好的i
的副本tmp = 4
,所以输出4
。
综上所述,当程序出现了不符合预期的结果时,去看看汇编就能够很好的理解这种差异。
再比如同样的一段代码,在VC++6.0上编译,运行,得到的输出截然不同:
其实这仍然是编译器编译出不同的汇编代码带来的差异,反汇编得到如下代码:
出现这种差异的原因是:不同于VS中"先计算,再统一写回"的策略,VC上是"边计算,边写回的"。总体流程如下:
- 对于参数
++i
,其效果等同于i = i + 1
,计算完毕后直接将i
的新值压栈。结果:_,_,4
- 对于参数
i++
,其效果等同于int tmp = i,i = i+1
,计算完毕后直接将tmp
压栈,结果:_,4,4
- 对于参数
i
,理论上来讲其值应该是5
,但VC中参数全部压栈后才执行第二个参数i++
中的i=i+1
(见汇编地址0x00401052),因为三个参数已经在栈中为printf
准备好,此次自增当然不会影响printf
的输出结果了。结果4,4,4
。