Chinaunix首页 | 论坛 | 博客
  • 博客访问: 36737
  • 博文数量: 10
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 179
  • 用 户 组: 普通用户
  • 注册时间: 2013-11-18 17:05
文章分类
文章存档

2014年(3)

2013年(7)

我的朋友

分类: C/C++

2014-04-24 22:44:20

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

 

[cpp] view plaincopy在CODE上查看代码片派生到我的代码片

  1. 华为笔试题  
  2. 1.写出判断ABCD四个表达式的是否正确若正确写出经过表达式中 a的值(3)  
  3. int a = 4;  
  4. (A)a += (a++); (B) a += (++a) ;(C) (a++) += a;(D) (++a) += (a++);  
  5. a = ?  
  6. 答:C错误,左侧不是一个有效变量,不能赋值,可改为(++a) += a;  
  7.   
  8. 改后答案依次为9,10,10,11  

可以看出,这个题除了测试你关于++aa++自加1是先生效还是后生效?以外,还要测试你对左值和右值的理解。

根据这个参考答案大胆的猜测一下过程:

 

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

B选项,a加上自身的前自增。注意:这个自增已经生效了,因为是赋值语句,等号“=”右边的表达式先生效?(到底赋值表达式左边右边怎么个生效顺序?下文也验证了,gcc把这个问题避免了,因为左边不允许出现这种形式!)等号右边的a5,左边的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换成了abcd四个变量(这些自加赋值表达式一定不要放在printf里,printf要单独放,因为自加导致打印结果不准确。)

[cpp] view plaincopy在CODE上查看代码片派生到我的代码片

  1. #include  
  2. //some unique and different useage of plusplus  
  3. main(){  
  4.         int a = 4;  
  5.         int b = 4;  
  6.         int c = 4;  
  7.         int d = 4;  
  8.   
  9.         a += (a++);  
  10.         b += (++b);  
  11. //who said that ++c could be work in linux C????  
  12. //      (c++) += c;  
  13. //      (++c) += c;  
  14. //      (++d) += d++;  
  15.   
  16.         printf("a = %d\n",a);  
  17.         printf("b = %d\n",b);  
  18.         printf("c = %d\n",c);  
  19.         printf("d = %d\n",d);  
  20. }  
  21. gcc编译结果:  
  22. aplusplus.c:12:8: error: lvalue required as left operand of assignment  
  23. aplusplus.c:13:8: error: lvalue required as left operand of assignment  
  24. aplusplus.c:14:8: error: lvalue required as left operand of assignment  
  25. 这三行分别指注释掉的三个语句~~  

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

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

 

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

gdb设断点,看下执行过程:

首先,a(值为0x4)和b(值为0x4)分别压入栈,地址分别是0x100x14

[cpp] view plaincopy在CODE上查看代码片派生到我的代码片

  1. 4        int a = 4;  
  2. 1: x/i $pc  
  3. => 0x80483ed :    movl   $0x4,0x10(%esp)  
  4. (gdb) si  
  5. 5        int b = 4;  
  6. 1: x/i $pc  
  7. => 0x80483f5 :    movl   $0x4,0x14(%esp)  

 

[cpp] view plaincopy在CODE上查看代码片派生到我的代码片

  1. Breakpoint 1, main () at aplusplus.c:9  
  2. 9       a += (a++);  
  3. 1: x/i $pc  
  4. => 0x804840d :    mov    0x10(%esp),%eax  

第九行是a += (a++);处相应断点,看下ab的自加过程。

[cpp] view plaincopy在CODE上查看代码片派生到我的代码片

  1. => 0x804840d :    mov    0x10(%esp),%eax  
  2.    0x8048411 :   add    %eax,%eax  
  3.    0x8048413 :   mov    %eax,0x10(%esp)  
  4.    0x8048417 :   addl   $0x1,0x10(%esp)  
  5.   
  6.    0x804841c :  addl   $0x1,0x14(%esp)  
  7.    0x8048421 :   mov    0x14(%esp),%eax  
  8.    0x8048425 :   add    %eax,%eax  
  9.    0x8048427 :   mov    %eax,0x14(%esp)  
  10. 。。。。。。  

先看a += a++;

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

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

eax再移回栈地址0x10

最后,给栈地址0x10中加入直接数18+1 == 9


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

然后从栈中移动b5)到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的位置,一个在最前,一个在最后。不知道其他版本的编译器,至少我这个版本的编译器把问题简化了,根本不允许在赋值符号”=“左边放++aa++一类语句,也就是说++a也不被认为是左值,所以根本没法区分赋值符号”=“左右的先后一说。

那么,还有临时变量一说么?再看两种情况:test++d += a++(因为之前ab都是和自己相加,这两个情况没测到)

[cpp] view plaincopy在CODE上查看代码片派生到我的代码片

  1. 8       int test = 5;  
  2. 1: x/i $pc  
  3. => 0x804840d :    movl   $0x5,0x2c(%esp)  
  4. (gdb)   
  5. 9       test++;  
  6. 1: x/i $pc  
  7. => 0x8048415 :    addl   $0x1,0x2c(%esp)直接把立即数加到test所占的栈空间0x2c  

 

[cpp] view plaincopy在CODE上查看代码片派生到我的代码片

  1. 22      d += a++;  
  2. 1: x/i $pc  
  3. => 0x804848c :   mov    0x1c(%esp),%eax     a移到eax寄存器中,  
  4. => 0x8048490 :   add    %eax,0x28(%esp)     a的值直接加到d所在内存地址中(d += a  
  5. => 0x8048494 :   addl   $0x1,0x1c(%esp)     将立即数1加给a  

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

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

如果在寄存器eax中的值算临时变量的话,那它其实还是原值,而不是自加1以后的值。

归根结底,那是C语言的定义,左值返回地址右值是无地址的表达式。一旦不在C语言层面看,很多东西都颠覆了,所以也不好这样论,C语言中的定义还按C语言的走吧,他说怎么算左值怎么算吧,知道实现过程就行了。

 

目前为止至少可以说,在这个环境下,以我通过ab发现的规律来推测,C选项的参考答案是错误的

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

D选项,如果可以的话,可能是:a先自加15,5*2 == 10以后,10再自加111

但这都是推测,不执行就不敢确认,况且人家的cd的自增可以在“=”左边,我使用的ab都是在“=”右边的情况,说不定会有特例。。。

以我的这个环境还真的没法测出来!遗憾,暂时不能完美解决这个问题。

但毕竟很多人都提到++a当左值的情况了,也许以前gcc有这样的。

 

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

 

为什么C++++++a可以而a++++不可以?

其实这取决于++左结合操作符号的操作函数,编译器中对于++a的调用相当于

int operator++ (int)

++右操作符号操作函数时,相当于这样,返回的依然是一个int型,所以无论++a的左边多少个都是可以的。

const int operator++()

注意这里返回的是一个const的,const只能作为右值,而不能作为左值的。

所以a++是可以的,但是a++++就不行,因为a++返回的是一个constint值,而该值是不能改变的,所以a++++不行。

 

 

 

常见小例子分析:

 

[cpp] view plaincopy在CODE上查看代码片派生到我的代码片

  1. #include  
  2. main(){  
  3.   
  4.         int a = 1;  
  5.   
  6. //      a = a +++++ a;//估计和下边带括号的执行顺序一样。  
  7.         a = a++ + ++a;  
  8. //      a = a + (++(++a));//前边也提到我的gcc是不允许++a当左值的,所以这种也不用试了  
  9.         printf("%d\n",a);  
  10. }  
  11. ~      

 

//如果写成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,下面是过程。

[cpp] view plaincopy在CODE上查看代码片派生到我的代码片

  1. 0x80483f5 : addl $0x1,0x1c(%esp)  
  2.   
  3. 0x80483fa : mov 0x1c(%esp),%eax  
  4.   
  5. 0x80483fe : add %eax,%eax  
  6.   
  7. 0x8048400 : mov %eax,0x1c(%esp)  
  8.   
  9. 0x8048404 : addl $0x1,0x1c(%esp)  

有人的机子号称跑出了4的结果,还是GCC,可惜没说什么版本,多少位。即使不知道自己的GCC什么版本,不知道自己系统的汇编怎么一个过程,他也能解释得跟结果一样:

a = a++ + ++a;

他的解释是a++的结果是1。然后++aa初始是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规范,而禁止gccc语言的扩展。
-std=iso9899:199409
使用C95规范
-std=c99
或者 -std=iso9899:1999 使用C99规范。
-std=gnu89
使用c89规范加上gcc自己的扩展(目前默认)
-std=gnu99
使用c99规范加上gcc自己的扩展

 

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

 

阅读(3479) | 评论(2) | 转发(3) |
0

上一篇:操作系统信号量问题——信号量的精简

下一篇:没有了

给主人留下些什么吧!~~

netel2014-04-30 15:19:26

我用gcc試了一下(C) (a++) += a;(D) (++a) += (a++);  這兩個都出錯了。
說明問題還是沒有明確答案。

幻の上帝2014-04-30 09:06:17

一个UB混上稀里糊涂地词法跟语义问题都能往汇编上扯。
你都知道看规范了,自己去看清楚会死么。