为什么函数printf的参数涉及到i++/++i时输出如此怪

一、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,我们有如下理解:

  1. ++i的效果等同于 i = i + 1,也就是立即将i的结果自增1并写回给i;
  2. i++的效果等同于int tmp = i , i = i + 1,也就说本次语句执行时仍然使用i的旧值tmp,执行完毕后i自增1写回给i;

按照以上逻辑,断言该代码输出的结果应该是:

>>3,3,5
>>5

但结果很奇怪,在VS中输出结果如下

>>5,4,5
>>5

第二行符合预期,但第一行和预期大相径庭。要搞明白为什么会这样,首先明白一件事:

  1. printf函数对参数的处理是从右至左的,也就是说虽然输出时的顺序为i,i++,++i,但处理的顺序却是++i >> i++ >> i;

可以简单地用一个程序证明:

int main()
{   
    int i = 0;
    int j = 0;
    printf("%d %d", j += i, i = 10);
}

输出结果:
在这里插入图片描述
然后需要明白另一件事:

  1. printf中对变量i不是立即写回,而是等到所有参数按照从右到左的顺序依次计算完毕后统一写回的,如果其它参数中改变了i的值,则写回时会对所有参数产生相同的影响。

可以通俗地理解为printf中存在一个缓冲区,先将所有对i的改变存在缓冲区中,最后统一写回。

重新分析结果:

    int i = 3;
    printf("%d %d %d", i, i++, ++i); //输出
    >>>5,4,5

从右往左处理参数:

  1. 首先 ++i会将i的值由3改变为4,但并不会立即写回;结果:_,_,_

  2. 然后是i++,后缀自增是比较特殊的情况,它会创建一个副本int tmp = i = 4,实际传给printf的是tmp而非i,所以在最后统一写回ii++不会产生影响(下边会再谈到);结果:_,4,_

  3. 接着是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上是"边计算,边写回的"。总体流程如下:

  1. 对于参数++i,其效果等同于i = i + 1,计算完毕后直接将i的新值压栈。结果:_,_,4
  2. 对于参数i++,其效果等同于int tmp = i,i = i+1,计算完毕后直接将tmp压栈,结果:_,4,4
  3. 对于参数i,理论上来讲其值应该是5,但VC中参数全部压栈后才执行第二个参数i++中的i=i+1(见汇编地址0x00401052),因为三个参数已经在栈中为printf准备好,此次自增当然不会影响printf的输出结果了。结果4,4,4
  • 19
    点赞
  • 43
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值