zoukankan      html  css  js  c++  java
  • C语言++a与a++的实现机制与操作符结合优先级

    看到一道“经典Linux C“面试题,关于左值和右值的。


    华为笔试题
    1.写出推断ABCD四个表达式的是否正确, 若正确, 写出经过表达式中 a的值(3分)
    int a = 4;
    (A)a += (a++); (B) a += (++a) ;(C) (a++) += a;(D) (++a) += (a++);
    a = ?
    答:C错误,左側不是一个有效变量。不能赋值,可改为(++a) += a;(补充:在我如今用的gcc中。++a也是不能当左值的)
    
    改后答案依次为9,10,10,11

    能够看出,这个题除了測试你关于++a与a++中“自加1是先生效还是后生效?”以外,还要測试你对左值和右值的理解。

    依据这个參考答案大胆的推測一下过程:


    A选项。a加上自身的后自增(还没有生效),得a的双倍,随后a的后自增生效,变成了2a+1,即9。

    B选项,a加上自身的前自增。

    注意:这个自增已经生效了,由于是赋值语句,等号“=”右边的表达式先生效(究竟赋值表达式左边右边怎么个生效顺序?下文也验证了,gcc把这个问题避免了,由于左边不同意出现这样的形式!)等号右边的a变5。左边的a随即也变成了5。所以是两个a的前自增(即4 + 1 == 5)相加(5 + 5)。结果10。

    C选项(“改”后),a的后自添加上a的自身,这里由于后自增(a++)是个“暂时变量”,没有内存地址(即右值)。所以不能用左赋值目标,替换成“左值”(++a),依据B选项等号右边先生效的原则,应该是4+4,之后再自加1,变成9才对(或者理解为4+1,再+4。反正没差别)。

    。。。

    反正顺序不正确,有冲突~!!

    D选项,a的前自加1(值为5)加上a的后自加1(为便于理解,写成5++),结果10,表达式结束后a的后自加1生效,结果11。


    有些小冲突!

    假设赋值表达式的符号“=”左边和右边有先后顺序(一般觉得右边先)的话。C就是错的。由于你不应该改变等号右边先运行的那部分~

    除非说++a在整个赋值表达式之前就生效。而a++在整个表达式结束时才生效。这样才干解释通!。!

    那么,事实到底怎样?

    还是做个程序測试了下的好,这样的比較迷惑人的东西一定要自己亲自操作一下。多试试条件,看看细小区别。

    由于这四个选项是反复的。所以把a换成了a、b、c、d四个变量(这些自加赋值“表达式”一定不要放在printf里,printf要单独放,由于自加导致打印结果不准确。)

    #include<stdio.h>
    //some unique and different useage of plusplus
    main(){
            int a = 4;
            int b = 4;
            int c = 4;
            int d = 4;
    
            a += (a++);
            b += (++b);
    //who said that ++c could be work in linux C?

    ??? // (c++) += c; // (++c) += c; // (++d) += d++; printf("a = %d ",a); printf("b = %d ",b); printf("c = %d ",c); printf("d = %d ",d); } gcc编译结果: aplusplus.c:12:8: error: lvalue required as left operand of assignment aplusplus.c:13:8: error: lvalue required as left operand of assignment aplusplus.c:14:8: error: lvalue required as left operand of assignment 这三行分别指凝视掉的三个语句~~

    实測发现,(c++)不能当做左值,(++c)和(++d)相同不行,和括号也没有关系,那么在我的 

    gcc (Ubuntu/Linaro 4.6.3-1ubuntu5) 4.6.3 
    貌似測试不了++a作为左值的情况了。


    这块弄不了,先挂起,看看左值右值的问题吧。依据描写叙述。右值通常是没有内存地址的。是暂时的,通俗点说。是个表达式,不是个值。

    用gdb设断点。看下运行过程:

    首先。a(值为0x4)和b(值为0x4)分别压入栈,地址各自是0x10和0x14

    4        int a = 4;
    1: x/i $pc
    => 0x80483ed <main+9>:    movl   $0x4,0x10(%esp)
    (gdb) si
    5        int b = 4;
    1: x/i $pc
    => 0x80483f5 <main+17>:    movl   $0x4,0x14(%esp)

    Breakpoint 1, main () at aplusplus.c:9
    9		a += (a++);
    1: x/i $pc
    => 0x804840d <main+41>:	mov    0x10(%esp),%eax
    
    第九行是a += (a++);处对应断点,看下a和b的自加过程。


    => 0x804840d <main+41>:	mov    0x10(%esp),%eax
       0x8048411 <main+45>:	add    %eax,%eax
       0x8048413 <main+47>:	mov    %eax,0x10(%esp)
       0x8048417 <main+51>:	addl   $0x1,0x10(%esp)
    
       0x804841c <main+56>:	addl   $0x1,0x14(%esp)
       0x8048421 <main+61>:	mov    0x14(%esp),%eax
       0x8048425 <main+65>:	add    %eax,%eax
       0x8048427 <main+67>:	mov    %eax,0x14(%esp)
    。

    。。。。。

    先看a += a++;

    a从栈地址0x10中移入eax寄存器中。

    在eax寄存器中自加(相当于double了一下4*2 == 8)。

    从eax再移回栈地址0x10。

    最后。给栈地址0x10中增加直接数1(8+1 == 9)


    然后b += ++b;
    先把直接数1加到b所在栈地址0x14中(4+1 == 5)。

    然后从栈中移动b(5)到eax寄存器中,

    在eax寄存器中自加(5*2 == 10),

    移动b回栈中地址0x14。


    结论:无论逻辑上怎么觉得,什么“++a为自加1先生效,a++为自加1后生效。暂时变量不可被赋值,等号左边右边谁先生效”。到最后。怎么实现都是编译器说了算,下面至少能算是我这个版本号的 gcc (Ubuntu/Linaro 4.6.3-1ubuntu5) 4.6.3 下的结论。


    a++和++b,中间过程类似。都是在eax寄存器中。用自己加自己(即乘以2),主要差别就是自加1的位置。一个在最前。一个在最后。不知道其它版本号的编译器。至少我这个版本号的编译器把问题简化了,根本不同意在赋值符号”=“左边放++a或a++一类语句,也就是说++a也不被觉得是左值,所以根本没法区分赋值符号”=“左右的先后一说。

    那么,还有暂时变量一说么?再看两种情况:test++和d += a++(由于之前a和b都是和自己相加,这两个情况没測到)

    8		int test = 5;
    1: x/i $pc
    => 0x804840d <main+41>:	movl   $0x5,0x2c(%esp)
    (gdb) 
    9		test++;
    1: x/i $pc
    => 0x8048415 <main+49>:	addl   $0x1,0x2c(%esp)直接把马上数加到test所占的栈空间0x2c中
    


    22		d += a++;
    1: x/i $pc
    => 0x804848c <main+168>:	mov    0x1c(%esp),%eax     把a移到eax寄存器中,
    => 0x8048490 <main+172>:	add    %eax,0x28(%esp)     a的值直接加到d所在内存地址中(d += a)
    => 0x8048494 <main+176>:	addl   $0x1,0x1c(%esp)     将马上数1加给a
    

    所谓暂时变量不暂时变量,至少从这个角度无法证明,尤其单独的test++。直接在原地址改动,当然有地址,当然是左值(不足之处是如今的这个是宏汇编,还不是单独的汇编命令,不够具体)。仅仅有在a += a++;之类更复杂的语句中才干体会这样的区别来,所以,这应该是编程语言和编译器之间协调的一个过程吧。编译器看要怎么来解决某种情况。怎么实现,解决不了就禁止了。

    假设真要我解释:(a++)是一个“没地址的暂时变量”的话,那依据上面的过程,我更愿意相信这个“暂时变量”根本没存在过

    假设在寄存器eax中的值算暂时变量的话,那它事实上还是原值,而不是自加1以后的值。

    归根结底,那是C语言的定义,“左值返回地址”、”右值是无地址的表达式“。

    一旦不在C语言层面看,非常多东西都颠覆了。所以也不好这样论,C语言中的定义还按C语言的走吧。他说怎么算左值怎么算吧,知道实现过程即可了。



    眼下为止至少能够说。在这个环境下,以我通过a、b发现的规律来猜測。C选项的“參考答案”是错误的

    C选项应该是a*2 == 8以后再自加1。应该是9;

    而D选项,假设能够的话,可能是:a先自加1变5,5*2 == 10以后,10再自加1变11。

    但这都是猜測。不运行就不敢确认,况且人家的c和d的自增能够在“=”左边。我使用的a和b都是在“=”右边的情况。说不定会有特例。

    。。

    以我的这个环境还真的没法測出来!

    遗憾。临时不能完美解决问题。

    但毕竟非常多人都提到++a当左值的情况了,或许曾经gcc有这种。


    还有非常多要注意的事。比方,C和C++ 是不一样的,C在不同的系统和不同的编译器下。结果也不同:


    为什么C++中++++a能够而a++++不能够?
    事实上这取决于++左结合操作符号的操作函数。编译器中对于++a的调用相当于
    int operator++ (int)
    而++右操作符号操作函数时,相当于这样,返回的依旧是一个int型,所以不管++在a的左边多少个都是能够的。
    const int operator++()
    注意这里返回的是一个const的,const仅仅能作为右值,而不能作为左值的。

    所以a++是能够的。可是a++++就不行,由于a++返回的是一个const的int值,而该值是不能改变的,所以a++++不行。




    常见小样例分析:


    #include<stdio.h>
    main(){
    
            int a = 1;
    
    //      a = a +++++ a;//预计和下边带括号的运行顺序一样。
            a = a++ + ++a;
    //      a = a + (++(++a));//前边也提到我的gcc是不同意++a当左值的。所以这样的也不用试了
            printf("%d
    ",a);
    }
    ~    

    //假设写成a = a +++++ a;会编译出错。

    root@v:/usr/local/C-language# gcc apppppa.c

    apppppa.c: In function ‘main’:apppppa.c:5:10: error: lvalue required as increment operand

    改动后。root@v:/usr/local/C-language# gcc apppppa.c

    root@v:/usr/local/C-language# ./a.out

    5

    非常特别的一点就是,”a+++++a“中。并非编译器简单的算顺序结合,此处空格非常重要。能改变性质。

    结果呢。没什么好说的,先+1变成2,然后2+2变成4,最后+1变成5。以下是过程。

    0x80483f5 <main+17>: addl $0x1,0x1c(%esp)
    
    0x80483fa <main+22>: mov 0x1c(%esp),%eax
    
    0x80483fe <main+26>: add %eax,%eax
    
    0x8048400 <main+28>: mov %eax,0x1c(%esp)
    
    0x8048404 <main+32>: addl $0x1,0x1c(%esp)

    有人的机子号称跑出了4的结果。还是GCC,可惜没说什么版本号,多少位。

    即使不知道自己的GCC什么版本号。不知道自己系统的汇编怎么一个过程。他也能解释得跟结果一样:

    a = a++ + ++a;

    他的解释是a++的结果是1。然后++a时a初始是2,++后变成3。结果就是a=1 + 3也就是4

    也就是说在第三个加号之前,a++在表达式中就已经生效了,那还要++a干嘛(真有这样的版本号的GCC?)所以这样的事,有点马后炮的感觉,你依据你机子的结果。推測这个结合过程和顺序,这全然没有不论什么意义,没有环境和结果让你说。那就没结论了。

    毕竟人家执行也出现了结果4,也不敢一棒子打死。保留意见吧。

    或许。他把表达式写在printf里了——那4就非常好解释了。



    既然都不同意当左值了。那么想当然:

    ++++a;

    a++++;这样的在我这都不可能同意。



    PS:

    怎样答这道题

    记得几点就好了,首先知道左值右值这样的基本概念,然后。能够”考虑“(仅仅是考虑,靠谱不靠谱须要进一步详查资料)说下一般觉得赋值表达式右边先运行。

    然后。拿出撒手锏,告诉他“和编译器有关。至少我的xxxx编译器是那样的~!”,

    然后能够试着“分析”:“我查看了Linux(AT&T)宏汇编,是把前自增放在整个式子前边,后自增放在整个表达式后边。把整个赋值语句当做一个总体。不分左右”


    假设有须要,能够进一步查C语言相关资料,这还包含不同版本号的差别,比方C99、ANSI C、C89、K&R C


    gcc下的语言规范设置:
    -std=iso9899:1990,-ansi或-std=c89 (三者全然等同)来指定全然依照c89规范,而禁止gcc对c语言的扩展。
    -std=iso9899:199409 使用C95规范
    -std=c99 或者 -std=iso9899:1999 使用C99规范。
    -std=gnu89 使用c89规范加上gcc自己的扩展(眼下默认)
    -std=gnu99 使用c99规范加上gcc自己的扩展


    不知道这是否能证明我这个结论和语言规范无关:



    =========================================================================================================================

    2016.02.21补充:

    讨论左右的自增顺序是说的赋值表达式"="两边,而不是“=”右側。这算同一边。在同一边的话,前自增都是前自增。后自增都是后自增。(又由于非常多编译器。比方gcc,不同意赋值操作符左側有自增操作。觉得这不是个左值,所以左边自增的问题也就不用讨论了,问题被简化了

    a = (++a) + (++a);//不会出现由于右边++a先运行而确定为x,左边的++a后运行和确定为x+1导致结果是2x+1的情况,结果事实上是2x+2。

    再不行改成后自增操作:

    a=(a++)+(a++);//表达式的结果是2x,可是那是表达式的结果。假设问你最后a是多少,那个后自增得算上,是2x+1。

    总之,表达式本身都是偶数的。。

    。。。

    a = x;

    b = a+++a;//b的值是表达式的值,就是(a++)+a;等于2x

    a = a+++a;//a的值,当时也是2x。可是过后有个后自增呢。所以过后再取的话a的值是2x+1

    a+++a;//由于有一个a的后自增在里边,所以最后a的值变成x+1。至于问表达式的值,就和上边b一样了。

    三者的差别要认清。或者也看是问你表达式结果还是a的值或者b的值。

    就看问题问的是什么东西,表达式。还是词句结束后某变量的值。


    由于在同一表达式内自增自减操作无关于顺序

    (a++)+a;与a+(a++);等价

    ++a+a;与a+(++a);等价



    另外。关于一长串自增符号的默认结合律,眼下的gcc看的话都是左结合的。

    a+++a;//等价于(a++)+a;

    a+++++a;//等价于((a++)++)+a;

    回想了一下前文a++ + ++a;//经过实际操作与和网友的交流,空格在语法上能起到括号的作用?听说是的。至少在运算符结合优先级上,是有所改变的。

    这样说也不正确,3*5+6是21。3* 5+6还是21。仅仅有3*(5+6)才是33.
    所以这个空格也就在自增运算符那才起到类似括号的作用,空格顶替括号不是常态。

     


    再參照一下优先级表,通常是单目运算符优于双目运算符。而自增相同也是优于加减法的,这个没疑问。就是“+”太多的时候,“+”究竟被看成自增还是看成加法比較头疼。



    -
    负号运算符
    -表达式
    右到左
    单目运算符
    (类型)
    强制类型转换
    (数据类型)表达式
     
    ++
    自增运算符
    ++变量名/变量名++
    单目运算符
    --
    自减运算符
    --变量名/变量名--
    单目运算符
    *
    取值运算符
    *指针表达式
    单目运算符
    &
    取地址运算符
    &左值表达式
    单目运算符
    !
    逻辑非运算符
    !表达式
    单目运算符
    ~
    按位取反运算符
    ~表达式
    单目运算符
    sizeof
    长度运算符
    sizeof 表达式/sizeof(类型)
     
    3
    /
    表达式/表达式
    左到右
    双目运算符
    *
    表达式*表达式
    双目运算符
    %
    余数(取模)
    整型表达式%整型表达式
    双目运算符
    4
    +
    表达式+表达式
    左到右
    双目运算符
    -
    表达式-表达式
    双目运算符
    5
    <<
    左移
    表达式<<表达式
    左到右
    双目运算符
    >>
    右移
    表达式>>表达式
    双目运算符
    后边的综合性不知道怎么解释的

     
    结合性
    ( ) [ ] -> . ++(后缀自增) --(后缀自减)
    left to right
    ! ~ ++(前缀自增) --(前缀自减) + - * sizeof(type)
    right to left
    这个优先级怎么理解。后缀是左往右。前缀是右往左?应该不是这个意思,由于实例是右往左根本没有机会,别说三个、哪怕五个“+”都不能抢两个过来,






    -
    负号运算符
    -表达式
    右到左
    单目运算符
    (类型)
    强制类型转换
    (数据类型)表达式
     
    ++
    自增运算符
    ++变量名/变量名++
    单目运算符
    --
    自减运算符
    --变量名/变量名--
    单目运算符
    *
    取值运算符
    *指针表达式
    单目运算符
    &
    取地址运算符
    &左值表达式
    单目运算符
    !
    逻辑非运算符
    !表达式
    单目运算符
    ~
    按位取反运算符
    ~表达式
    单目运算符
    sizeof
    长度运算符
    sizeof 表达式/sizeof(类型)
     
    3
    /
    表达式/表达式
    左到右
    双目运算符
    *
    表达式*表达式
    双目运算符
    %
    余数(取模)
    整型表达式%整型表达式
    双目运算符
    4
    +
    表达式+表达式
    左到右
    双目运算符
    -
    表达式-表达式
    双目运算符
    5
    <<
    左移
    表达式<<表达式
    左到右
    双目运算符
    >>
    右移
    表达式>>表达式
    双目运算符
  • 相关阅读:
    正则表达式练习
    Linux下文件删除的原理
    (转)linux grep 正则表达式
    linux 需要记忆的知识
    linux 常用命令
    TestNG测试方法
    TestNG配置注解
    jquery 获取和设置 select下拉框的值
    Kings(状压DP)
    Tirp(状压DP)
  • 原文地址:https://www.cnblogs.com/gcczhongduan/p/5239842.html
Copyright © 2011-2022 走看看