为什么函数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
<think>在C语言中,`i++`和`++i`都是自增运算符,用于将变量`i`的值增加1,但它们在表达式中的行为有重要区别: 1. **i++(后缀自增)**:先返回`i`的当前值,然后再将`i`的值增加1。 2. **++i(前缀自增)**:先将`i`的值增加1,然后返回增加后的值。 在for循环中,当它们单独作为循环步进表达式(即`for`的第三个表达式),由于不涉及表达式的值,因此效果相同。但是,如果在循环体内或条件表达式中使用,则会产生不同的效果。 ### 示例1:单独使用(效果相同) ```c // 使用i++ for (int i = 0; i < 5; i++) { printf("%d ", i); // 输出: 0 1 2 3 4 } // 使用++i for (int i = 0; i < 5; ++i) { printf("%d ", i); // 输出: 0 1 2 3 4 } ``` - 原因:在`for`循环的第三个表达式中,自增操作是独立语句,不参与表达式求值,因此两种写法效果完全一致。 ### 示例2:在表达式中使用(效果不同) ```c int i = 0; while (i < 5) { printf("i++: %d ", i++); // 输出: 0 1 2 3 4 } int j = 0; while (j < 5) { printf("++j: %d ", ++j); // 输出: 1 2 3 4 5 } ``` - 解释: - `i++`:先打印`i`的当前值,再自增(打印0,然后i变为1) - `++j`:先自增,再打印(j先变为1,然后打印1) ### 性能考虑(现代编译器优化后无区别) - 在非内置类型的重载运算符中(如C++),`++i`可能更高效,因为`i++`需要保存临值。 - 在C语言基本类型(如`int`)中,现代编译器优化后性能相同。 ### 关键点总结 | 特性 | `i++` (后缀) | `++i` (前缀) | |--------------|------------------------|------------------------| | 返回值 | 自增前的值 | 自增后的值 | | 副作用发生点 | 表达式求值后立即自增 | 表达式求值前自增 | | 单独语句性能 | 相同(编译器优化) | 相同(编译器优化) | | 复合表达式 | 可能产生非预期结果 | 通常更符合直觉 | ### 错误使用案例 ```c int arr[] = {10, 20, 30}; int i = 0; // 错误:访问越界和未定义行为 printf("%d", arr[i++]); // 输出arr[0]=10,然后i=1 printf("%d", arr[++i]); // 输出arr[2]=30,因为i先变成2 // 正确:明确拆分步骤 int j = 0; printf("%d", arr[j]); // 输出10 j++; // j=1 j++; // j=2 printf("%d", arr[j]); // 输出30 ``` §§相关问题§§ 1. 在while循环条件中使用`i++`和`++i`会有什么不同效果? 2. 对于自定义数据类型(如C++类),为什么推荐使用`++i`而不是`i++`? 3. 表达式`a = i++ + ++i`为什么是未定义行为? 4. 在函数调用参数中同使用多个自增运算符可能导致什么问题? 5. C语言中自减运算符`--`的前缀和后缀形式与自增运算符有何异同?</think>在C/C++中,`i++`(后缀自增)和`++i`(前缀自增)都用于将变量`i`的值增加1,但它们在表达式中的行为有重要区别: ### 核心区别 | 特性 | `i++` (后缀自增) | `++i` (前缀自增) | |--------------|--------------------------|--------------------------| | **返回值** | 返回自增前的原始值 | 返回自增后的新值 | | **执行顺序** | 先返回值,后自增 | 先自增,后返回值 | | **性能影响** | 可能产生临对象(复杂类型) | 通常更高效 | ### 在for循环中的使用 当单独作为循环步进表达式(无嵌套使用),两者**效果相同**: ```c // 两种写法完全等效 for (int i = 0; i < 5; i++) { ... } // 输出: 0 1 2 3 4 for (int i = 0; i < 5; ++i) { ... } // 输出: 0 1 2 3 4 ``` 原因:for循环的第三个表达式不关心返回值,只需完成自增操作 ### 关键区别场景(当需要返回值) ```c int main() { int a = 5, b = 5; // 示例1:在赋值语句中 int x = a++; // x = 5, 然后a=6 int y = ++b; // b=6, 然后y=6 // 示例2:在函数参数printf("i++: %d\n", a++); // 输出6,然后a=7 printf("++i: %d\n", ++b); // b=7,输出7 // 示例3:复合表达式 int c = 10; int d = c++ + c++; // 未定义行为!不同编译器结果不同 int e = ++c + ++c; // 同样未定义行为! return 0; } ``` ### 性能差异(重要) 1. **基本类型(int/char等)**:现代编译器优化后无差异 2. **复杂类型(C++对象)**: ```cpp class Counter { public: Counter& operator++() { // ++i ++value; // 直接修改 return *this; // 返回自身引用 } Counter operator++(int) { // i++ Counter temp = *this; // 创建临副本 ++value; // 修改自身 return temp; // 返回副本(额外开销!) } private: int value = 0; }; ``` - `++i` 无临对象,效率更高 - `i++` 需构造/析构临对象,效率较低 ### 最佳实践 1. **for循环中**:优先使用`++i`(性能习惯) 2. **独立语句**:使用`i++;`或`++i;`效果相同 3. **避免**: ```c // 禁止在复杂表达式中混用 arr[i++] = arr[++i]; // 未定义行为! // 禁止在同一变量多次自增 func(i++, i++); // 参数求值顺序未定义 ``` ### 特殊案例:指针运算 ```c int arr[] = {10, 20, 30}; int *p = arr; printf("%d ", *p++); // 输出10,p指向arr[1] printf("%d ", *++p); // p先指向arr[2],输出30 ```
评论 2
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值