c/c++专题-常考易错点(i++) + (++i)计算原理分析

目录

杂言

问题分析

单个表达式输出结果

多个表达式加赋值输出结果

赋值给自己的问题,值永远不变

函数传值 fun(i++, ++i, ++i, --i)的形式

总结


杂言

  对它简单处理,它就简单;
  对它复杂处理,它就复杂;
  复杂难以把控,无法预测;
  可预测的程度,关系成败;

问题分析

最近在csdn上的问题区有很多关于i++的表达式的求值输出和使用有很多的问题,这边总结了各种情况。

说明下,下面的代码运行环境的编译器是gcc,大家可以在其它环境可以多试试, 比如vc上

主要有几类:

  • 单个表达式输出结果,例如 printf ("%d",  (i++) + (++i) )
  • 多个表达式加赋值输出结果,例如 printf("%d, %d\n", i+=1, i++)
  • 赋值给自己的问题,值永远不变,例如 i = i++;
  • 函数传值 fun(i++, ++i, ++i, --i)的形式,printf只是个特例

单个表达式输出结果

     先看如下3个运行的结果:

#include <stdio.h>
int main()
{
    int i=3, j=3, k=3;
 
    printf("%d\n",(++j)+(++j));

    printf("%d\n",(++i)+(++i)+(++i));

    printf("%d\n",(++k)+(++k)+(++k)+(++k));

    return 0;
}

输出结果
    10
    16
    23 

结果分析
    输出a的结果是10,16,23 而不是9,15,22

  我们来分析一下 (++i)+(++i)+(++i) 这个表达式计算过程
  按照一般的理解如果i=3,(++i)+(++i)+(++i) 是改写成 4 + 5 +6 = 15 但是答案不对

  那么需要找到一种方法看它到底是怎么运行的,最直接的方法去看下编译完成后汇编语言是怎么  个执行顺序,为啥汇编能看清楚呢,那是因为程序需要编译成一条一条可执行的语句,一条语句就是两个操作数一个操作符,可以理解是最简单的一条语句不能再分解了,这样就不会有二义性。

  我们来看下简单的代码看看汇编怎么样的,代码如下, 文件名叫 htest.c :

#include <stdio.h>
int main()
{
    int i=3;
    int a=0;
 
    a = (++i)+(++i);
    printf("%d\n", a);

    return 0;
}

通过 gcc -S ./htest.c  得到一个htest.s 编译后的中间文件,里面包含了汇编指令, 如下

 说明:rbp是指栈指针   
    subq	$16, %rsp
	movl	$3, -4(%rbp)   // int i=3; 给i赋值
	movl	$0, -8(%rbp)   // int a=0; 给a赋值
	addl	$1, -4(%rbp)   // ++i;  执行一次++i,i=4
	addl	$1, -4(%rbp)   // ++i;  执行一次++i,i=5
	movl	-4(%rbp), %eax // i+i;  执行2次++后开始计算 5+5
	addl	%eax, %eax     // a = i+i; 把计算结果给 a,结果是10 
	movl	%eax, -8(%rbp) // 
	movl	-8(%rbp), %eax
	movl	%eax, %esi
	movl	$.LC0, %edi
	movl	$0, %eax
	call	printf
	movl	$0, %eax
	leave

  从上面的汇编分析很容易看出来 i 是执行了2次 ++ 操作后,才开始执行 a = i+i 操作。具体的汇编指令不需要理解很深,大致看下操作指令add和mov就大概知道他们是什么意思了。

  总结
      通过跟踪发现对于这种单表达式 a = (++i)+(++i)+(++i) 有如下规则:我们先改写下
      a = a1 + a2 +a3   其中 a1=a2=a3=++i, i=3;

  •  第一步:先取出来一个完整的计算,包括一个操作符 + 号,两个操作数a1和a2,这个时候就开始计算了
  • 第二步:取出a1+a2这个完整计算式后开始计算,因为是++号前置所以先加1, a1和a2都先执行加1操作, 这个时候i=5,所以 a1+a2=10
  • 第三步:重复第一步,取出一个完整的表达式, 整个表达式可以改写成 a = 10 + a3, 那么先a3加执行1 之后 i=6,  所以 a = 10 + 6 =16

      可以自测下面两种case,有问题可以讨论讨论

  •  后置i++
  • (i+=1) + (++i)

多个表达式加赋值输出结果

    先看如下运行的结果

#include <stdio.h>
int main()
{
    int i=3;

    printf("%d %d %d\n", ++i, ++i, i+=2);

    return 0;
}

输出结果:
    7 7 7

结果分析:
    不是4 5 7

我们来分析一下,看起来在printf里面,后面的表达式都计算完成了后才开始打印的, 来通过汇编来验证一下 

	subq	$16, %rsp
	movl	$3, -4(%rbp)   //i=3赋值
	addl	$2, -4(%rbp)   //计算i+=2 i=5
	addl	$1, -4(%rbp)   //计算++i  i=6
	addl	$1, -4(%rbp)   //计算++i  i=7
	movl	-4(%rbp), %ecx  //printf参数1 =i=7  
	movl	-4(%rbp), %edx  //printf参数2 =i=7
	movl	-4(%rbp), %eax  //printf参数3 =i=7
	movl	%eax, %esi
	movl	$.LC0, %edi
	movl	$0, %eax
	call	printf
	movl	$0, %eax
	leave

总结

    通过上面汇编的分析,printf是从右到左开始计算参数,先计算 i+=2, 在计算两个++i。所有的参数计算完了后再开始传参数,并且++操作符合我们的想法,还可以测试以下例子,++后置

printf("%d %d %d\n", i++, ++i, i+=2);

赋值给自己的问题,值永远不变

先看如下运行的结果

#include <stdio.h>
int main()
{
    int i=0;

    while(i<3) {
        printf("%d\n", i);

        i = i++; //重点分析
    }

    return 0;
}

输出结果:
    死循环了, 并且i的值一直是0

结果分析:
    看起来有 i++,怎么死循环了呢

  来分析一下,为啥结果是0呢,还是老样子分析如下简单的代码的汇编

#include <stdio.h>
int main()
{
    int i=0;

    i = i++;

    return 2;
}

  汇编代码如下:

	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	movl	$0, -4(%rbp)      //i=0 赋值
	movl	-4(%rbp), %eax    //保存i的值到eax, eax=0
	leal	1(%rax), %edx
	movl	%edx, -4(%rbp)    //i++操作 i=1
	movl	%eax, -4(%rbp)    //eax=0 又赋值给了 i,i=0
	movl	$2, %eax
	popq	%rbp
	.cfi_def_cfa 7, 8

总结:可以看出上面的运行是 有一个临时变量 t = i;之后 i++  i=1;再之后 i=t,又转回来了,i的值没有变化
    

函数传值 fun(i++, ++i, ++i, --i)的形式

先看下面代码    

#include <stdio.h>

void func(int a, int b, int c, int d)
{
    printf("%d,%d,%d,%d\n", a, b, c, d);
}

int main()
{
    int i=3;

    func(i++, ++i, i++, --i);

    return 2;
}

结果输出:
    4,5,2,5

  来看下汇编代码

	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	subq	$16, %rsp
	movl	$3, -4(%rbp)     i=3;
	subl	$1, -4(%rbp)     --i -> i=2
	movl	-4(%rbp), %edx   edx = i = 2 (第三个参数 缓存i=2的值)
	leal	1(%rdx), %eax    eax = 1 + 2 = 3
	movl	%eax, -4(%rbp)   i = 3
	addl	$1, -4(%rbp)     ++i -> i = 4
	movl	-4(%rbp), %eax   eax = 4
	leal	1(%rax), %ecx    ecx = 5
	movl	%ecx, -4(%rbp)   i = 5
	movl	-4(%rbp), %ecx   ecx = 5  (第四个参数 以最终i为参数)
	movl	-4(%rbp), %esi   esi = 5  (第二个参数 以最终i为参数)
	movl	%eax, %edi       edi = 4  (第一个参数 缓存i=4的值)
	call	_Z4funciiii
	movl	$2, %eax
	leave

 说明一下:
      从上面跟踪汇编代码可以看出来,有如下规则

  •   函数的参数先是从右到左开始计算参数,
  •   对于i++,i--,是算到当前位置会缓存一份值,然后在立即加1或者减1,缓存的值为参数的值,比如上面的 i++,--i, 先算--i i=2,这个时候遇到i++,先缓存这个2作为参数,在加1
  •   对于++i,--i,先加1,然后等所有的参数都算完后,最终的i的值作为参数
     比如上面的 func(i++, ++i, i++ , --i); 四个计算完成后才传值最终是5

  有点复杂,可以在多测试一下

总结

    写代码的时候尽量简单,预期结果很明确的标准。遇到问题我们可以慢慢想办法去分解一下。知识尽量的全面和深入,关键时刻还是管用的

    有什么不对的地方请指出,大家多讨论讨论

评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

X-道至简

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值