看到一道“经典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\n",a);
printf("b = %d\n",b);
printf("c = %d\n",c);
printf("d = %d\n",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在不同的系统和不同的编译器下,结果也不同:
常见小例子分析:
#include<stdio.h>
main(){
int a = 1;
// a = a +++++ a;//估计和下边带括号的执行顺序一样。
a = a++ + ++a;
// a = a + (++(++a));//前边也提到我的gcc是不允许++a当左值的,所以这种也不用试了
printf("%d\n",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;//经过实际操作与和网友的交流,空格在语法上能起到括号的作用?听说是的!至少在运算符结合优先级上,是有所改变的。
所以这个空格也就在自增运算符那才起到类似括号的作用,空格顶替括号不是常态。
再参照一下优先级表,一般是单目运算符优于双目运算符,而自增同样也是优于加减法的,这个没疑问,就是“+”太多的时候,“+”到底被看成自增还是看成加法比较头疼。
-
|
负号运算符
|
-表达式
|
右到左
|
单目运算符
| |
(类型)
|
强制类型转换
|
(数据类型)表达式
| |||
++
|
自增运算符
|
++变量名/变量名++
|
单目运算符
| ||
--
|
自减运算符
|
--变量名/变量名--
|
单目运算符
| ||
*
|
取值运算符
|
*指针表达式
|
单目运算符
| ||
&
|
取地址运算符
|
&左值表达式
|
单目运算符
| ||
!
|
逻辑非运算符
|
!表达式
|
单目运算符
| ||
~
|
按位取反运算符
|
~表达式
|
单目运算符
| ||
sizeof
|
长度运算符
|
sizeof 表达式/sizeof(类型)
| |||
3
|
/
|
除
|
表达式/表达式
|
左到右
|
双目运算符
|
*
|
乘
|
表达式*表达式
|
双目运算符
| ||
%
|
余数(取模)
|
整型表达式%整型表达式
|
双目运算符
| ||
4
|
+
|
加
|
表达式+表达式
|
左到右
|
双目运算符
|
-
|
减
|
表达式-表达式
|
双目运算符
| ||
5
|
<<
|
左移
|
表达式<<表达式
|
左到右
|
双目运算符
|
>>
|
右移
|
表达式>>表达式
|
双目运算符
|
结合性
| |
---|---|
( ) [ ] -> . ++(后缀自增) --(后缀自减)
|
left to right
|
! ~ ++(前缀自增) --(前缀自减) + - * sizeof(type)
|
right to left
|
-
|
负号运算符
|
-表达式
|
右到左
|
单目运算符
| |
(类型)
|
强制类型转换
|
(数据类型)表达式
| |||
++
|
自增运算符
|
++变量名/变量名++
|
单目运算符
| ||
--
|
自减运算符
|
--变量名/变量名--
|
单目运算符
| ||
*
|
取值运算符
|
*指针表达式
|
单目运算符
| ||
&
|
取地址运算符
|
&左值表达式
|
单目运算符
| ||
!
|
逻辑非运算符
|
!表达式
|
单目运算符
| ||
~
|
按位取反运算符
|
~表达式
|
单目运算符
| ||
sizeof
|
长度运算符
|
sizeof 表达式/sizeof(类型)
| |||
3
|
/
|
除
|
表达式/表达式
|
左到右
|
双目运算符
|
*
|
乘
|
表达式*表达式
|
双目运算符
| ||
%
|
余数(取模)
|
整型表达式%整型表达式
|
双目运算符
| ||
4
|
+
|
加
|
表达式+表达式
|
左到右
|
双目运算符
|
-
|
减
|
表达式-表达式
|
双目运算符
| ||
5
|
<<
|
左移
|
表达式<<表达式
|
左到右
|
双目运算符
|
>>
|
右移
|
表达式>>表达式
|
双目运算符
|