• 一道简单的题目引发的思考


    ——Don't believe in magic !Understand what your program do ,how they do .

    引言

    昨晚一时兴起,我脑子就问自己下面的代码会输出什么,也不知道我脑子为什么有这个代码模型,只是模糊的有些印象:

    #include <stdio.h>
    #include <stdlib.h>
    
    int main(int argc,char** argv)
    {
        int i=3,j;
        j=(i++)+(i++)+(++i);
        printf("i = %d, j = %d\n",i,j);
        exit(0);
    }

    您会怎样考虑这个问题呢?您不运行这个程序能准确地说出答案吗?我猜想肯定有大部分人不能肯定且准确地说出答案!如果您不能,这篇文章就是为你准备的,保证您看完之后豁然开朗!请细看下文,outline如下:

    • 1、诸君的回答
      • 1.1、A君的回答
      • 1.2、B君的回答
      • 1.3、C君的回答
      • 1.4、D君的回答
    • 2、编译器的输出
      • 2.1、Visual Studio的输出
      • 2.2、GCC的输出
      • 2.3、Visual C++ 2010的输出
    • 3、分析
      • 3.1、gcc编译器上的分析
      • 3.2、分析gcc编译之后的汇编代码
      • 3.3、vs编译器上的分析
      • 3.4、分析VS编译之后的汇编代码
    • 4、扩散思维
      • 4.1、思维放射
      • 4.2、VS的输出
      • 4.3、GCC的输出
    • 5、感慨

    1、诸君的回答

    我那这道题目问了几个人,他们的答案不尽相同。

    1.1、A君的回答

    因为i = 3,故依次i++=4,i++=5,++i=6,i最后输出为i = 6;但是由于前面两个++是后置++,最后一个++是前置++,故j = 3+4+6 = 13。

    1.2、B君的回答

    因为i = 3,故第一个i++后为4,第二个i++后为5,接着做i+i操作 = 5+5=10,最后与(++i)相加 = 10+6=16。

    1.3、C君的回答

    因为i = 3,故依次i++=4,i++=5,++i=6,i最后输出为i = 6;但是第一i、第二个i的++是后置++,先进行i+i操作,然后进行两次i++后置操作,故等价于(i)+(i) = 3+3=6,i++,i++,最后与++i=6相加等于12。

    1.4、D君的回答

    因为i = 3,故依次i++=4,i++=5,++i=6,i最后输出为i = 6;但是前面两个++都是后置++,故先做i+i+(++i)操作,然后才在i++,i++操作,第三个++是前置++,故等价于 i+i+(++i)=3+3+4=10,i++,i++。

    到底哪个人说得对呢?

    2、编译器的输出

    首先让我们先来看看编译器会输出什么?

    2.1、Visual Studio的输出

    运行环境:Win7+VS2005 or VS2010,输出如下图所示:

    image

    2.2、GCC的输出

    运行环境:Ubuntu 10.04+gcc (Ubuntu 4.4.3-4ubuntu5) 4.4.3,运行结果如下:

    image

    2.3、Visual C++的输出

    运行环境:Win7+VC2010,输出和VS一样,及i = 6 & j = 12

    看到这里你肯定想问why? why?? why???

    3、分析

    重编译器的输出结果来看貌似C君、D君的分析都是对的,这种差异跟编译器有直接的关系,因为对于这个表达式怎么编译还没有形成标准,编译器的结合方向不同,答案因此会有所不同。而且当然还包括运算符的优先级等。其实顶多算C君答对了一部分,其他几个人的回答都是错的,详情见下面的分析。

    3.1、gcc编译器上的分析

    (i++)+(i++)+(++i) <=> i+i+(++i); i++; i++;即如果表达式中含有i++,一律替换成i,然后在表达式之后进行i++操作。

    这样的话上面的代码就可以很好的理解了,即3+3+4=10。

    3.2、分析gcc编译之后的汇编代码

    可以对gcc编译之后的执行文件进行反编译分析验证正确性。在Linux下面可以用objdump –d xxx(执行文件)命令反汇编执行文件。反编译之后可以看到如下图所示的代码:

    gcc反汇编之后的代码

    说明:Linux下采用的是AT&T的汇编语法格式,Windows下面采用的是Intel汇编语法格式。二者的主要区别在于:

    1. 指令操作数的赋值方向是不同的
         Intel:第一个是目的操作数,第二个是源操作数
         AT&T:第一个是源操作数,第二个是目的操作数
    2. 指令前缀
         AT&T:寄存器前边要加上%,立即数前要加上$
         Intel:没有这方面的要求
    3. 内存单元操作数
         Intel:基地址使用[]
         AT&T:  基地址使用()
        比如:intel中  mov  ax,[bx]
                    AT&T中 movl (%eax),%ebx
    4. 操作码的后缀
           AT&T中操作码后面有一个后缀字母:“l” 32位,“w” 16位,“b” 8位
           Intel却使用了在操作数前面加dword ptr, word ptr, byte ptr的格式
         例如:mov al,bl (Intel)
                   movb %bl %al (AT&T)
    5. AT&T中跳转指令标号后的后缀 表示跳转方向,“f”表示向前,“b”表示向后

    下面我们重点分析红框中的代码:

    movl  $0x3 ,0x1c(%esp):将3赋给i,即i=3
    mov   0x1c(%esp) ,%eax:将esp中的i放到eax中
    add     %eax ,%eax:进行i+i操作,即3+3
    addl    $0x1 ,0x1c(%esp):对i进行加1操作,即表达式中的(++i)
    add     0x1c(%esp),%eax:将eax中i+i的结果6,加上++i之后的i,即6+4=10
    addl    $0x1 ,0x1c(%esp):对i进行加1操作,即表达式中的(i++)
    addl    $0x1 ,0x1c(%esp):对i进行加1操作,即表达式中的(i++)

    至此关键代码已经分析完成,由此可见我们之前对gcc编译器上的分析是正确的。

    3.3、vs编译器上的分析

    (i++)+(i++)+(++i) <=>(++i)+i+i; i++; i++;即如果表达式中含有前置++i,首先执行++i操作;表达式中的i++,一律换成i,然后执行加法操作;最后在进行i++操作。

    这样的话上面的代码就可以很好的理解而来,即首先执行++i,i变为4了;然后进行i+i+i=4+4+4;i++,i++。

    其实对于VS/VC2010编译器中的可以总结为:当用于四则运算时,前置++/--的运算优先级最高,后置++/--的运算优先级最小,其它的居中。(跟你书上看到是不是不同!)

    3.4、分析VS编译之后的汇编代码

    用W32Dasm反汇编vs编译生成的exe文件,追踪代码。我们可以看到如下图所示的代码:

    反汇编后的代码

    下面重点分析一下框中代码:

    mov [ebp-08],3:将3赋给i,即i=3
    mov eax,dword ptr [ebp-08]:将ebp中的i的值放到eax中,是"累加器"(accumulator), 它是很多加法乘法指令的缺省寄存器。dword ptr表示这是一个双字指针,即所要寻址的数据是一个双字(4字节)
    add eax,1:对eax中的i进行加1操作
    mov dword ptr [ebp-08] ,eax:将eax中的i赋给ebp中i,即将i加1之后的值赋给i,也即达到i=i+1的效果
    mov ecx,dword ptr [ebp-08]:将ebp中的i放到ecx中
    add ecx,dword ptr [ebp-08]:将ebp中的值加上i,即4+4
    add ecx,dword ptr [ebp-08]:将ebp中的值加上i,即4+4+4
    mov dword ptr [ebp-14],ecx:将ecx中的值赋给j
    mov edx,dword ptr [ebp-08]:将i放到edx中
    add edx,1:对edx中的i进行加1操作
    mov dword ptr [ebp-08] ,edx:将edx中的i赋给ebp中i,即将i加1之后的值赋给i,也即达到i=i+1的效果
    mov eax,dword ptr [ebp-08]:将i放到eax中
    add eax,1:对eax中的i进行加1操作
    mov dword ptr [ebp-08] ,eax:将eax中的i赋给ebp中i,即将i加1之后的值赋给i,也即达到i=i+1的效果

    至此,上面表达式的关键运算部分已经分析完成。从这里可以知道,上面我们地VS编译器的分析是正确的。

    4、发散思维

    可以说通过上面那么篇幅的介绍,我们对涉及前置++和后置++的加法运算表达式的计算过程有了一个清楚的认识,下面就我们发散一下我们的思维,释放我们的能量。

    4.1、思维放射

    您看下面的代码会输出什么,现在知道了吧!

    #include <stdio.h>
    #include <stdlib.h>
    
    int main(int argc,char** argv)
    {
        int i=3,j=3,k=3,l=3,m=3,n=3,result1,result2,result3,result4,result5,result6;
        result1=(++i)+(++i);
        printf("i = 3\n");
        printf("result1= (++i)+(++i) = %d\n\n",result1);
    
        result2=(j++)+(j++);    
        printf("j = 3\n");
        printf("result2= (j++)+(j++) = %d\n\n",result2);
    
        result3=(++k)+(++k)+(++k);
        printf("k = 3\n");
        printf("result3= (++k)+(++k)+(++k) = %d\n\n",result3);
    
        result4=(++l)+(++l)+(l++);
        printf("l = 3\n");
        printf("result4= (++l)+(++l)+(l++) = %d\n\n",result4);
    
        result5=(m++)+(m++)+(m++);
        printf("m = 3\n");
        printf("result5=(m++)+(m++)+(m++) = %d\n\n",result5);
    
        result6=(n++)+(++n)+(n++);
        printf("n = 3\n");
        printf("result6=(n++)+(++n)+(n++) = %d\n\n",result6);
        exit(0);
    }

    请不看结果先自己分析一下,然后和结果对比!

    4.2、VS的输出

    运行环境:Win7+VS2005 or VS2010,输出如下图所示:

    image

    4.3、GCC的输出

    运行环境:Ubuntu 10.04+gcc (Ubuntu 4.4.3-4ubuntu5) 4.4.3,运行结果如下:

    image

    根据前面我们挖掘到的规则,我们可以得到result3之外所有其它答案。最后,还有一点要说明的是:gcc中的加法运算表达死中,是按照从左到右按顺序,如果运算符两边有++i操作数,就先进行++i操作,然后进行加法运算;vs中的加法运算表达式中,则不一样,只要表达式中有++i操作数,就要先计算,最后才是进行加法运算。这也是为什么result3不同的原因!加法运算可以扩展到减法、乘法、除法运算和前置--、后置--。但是如果是四则混合运算还要考虑加、减、乘、除的优先级问题。

    5、感慨

    通过这么多分析,我们可以算得上是对涉及++、--的运算表达式计算过程有了透彻理解!我在挖掘这个计算过程的路上,可是化了不少功夫也在刚开始分析汇编代码时遇到了一些困难,但这颗求知的心,推动着我坚持要去弄清楚它!最后我想说:请不要写这种语句!理由很简单,它既不好理解又不好维护,最重要的是它的结果会因编译器的不同而不同。


    作者:吴秦
    出处:http://www.cnblogs.com/skynet/
    本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名吴秦(包含链接).

  • 相关阅读:
    【学习总结】Git学习-参考廖雪峰老师教程二-安装Git
    【学习总结】Git学习-参考廖雪峰老师教程一-Git简介
    【学习总结】vi/vim命令是使用
    【学习总结】Git学习-参考廖雪峰老师教程-总
    【kindle笔记】之 《鬼吹灯》-9-20
    【学习总结】win7下安装Ubuntu双系统的日常
    【kindle笔记】之 《明朝那些事儿》-2018-7-1
    【kindle笔记】之 《黑客微百科》-2018-6-17
    【kindle笔记】之 《恶意》-2018-4-20
    【kindle笔记】之 《活着》-2018-2-5
  • 原文地址:https://www.cnblogs.com/skynet/p/1775084.html
Copyright © 2020-2023  润新知