看到一道“经典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在不同的系统和不同的编译器下。结果也不同:

为什么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\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自己的扩展

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

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvaHVxaW53ZUk5ODc=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" alt="">

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

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
<<
左移
表达式<<表达式
左到右
双目运算符
>>
右移
表达式>>表达式
双目运算符

C语言++a与a++的实现机制与操作符结合优先级的更多相关文章

  1. C语言中操作符的优先级大全

    C语言中操作符的优先级大全, 当然c++, Objective-C,大部分语言都试用. 下面是来自The C Programming Language 2th的总结. OperatorsAssocia ...

  2. 【UML 建模】UML建模语言入门-视图,事物,关系,通用机制

    . 作者 :万境绝尘  转载请注明出处 : http://blog.csdn.net/shulianghan/article/details/18964835 . 一. UML视图 1. Ration ...

  3. const分别在C和C++语言里的含义和实现机制

    const的含义        简单地说:const在c语言中表示只读的变量,而在c++语言中表示常量. C语言 const是constant的缩写,是恒定不变的意思,也翻译为常量,但是很多人都认为被 ...

  4. UML建模语言入门-视图,事物,关系,通用机制

    . 作者 :万境绝尘  转载请注明出处 : http://blog.csdn.net/shulianghan/article/details/18964835 . 一. UML视图 1. Ration ...

  5. 探究 Go 语言 defer 语句的三种机制

    Golang 的 1.13 版本 与 1.14 版本对 defer 进行了两次优化,使得 defer 的性能开销在大部分场景下都得到大幅降低,其中到底经历了什么原理? 这是因为这两个版本对 defer ...

  6. 【C语言C++编程入门】——编译机制和语言标准!

    编程机制 编写程序时必须遵循确切步骤主要是取决于你的计算机环境.因为 C语言是可以移植的,所以它在许多环境中可用,其中包括 UNIX,Linux,Windows等等 . 不过,让我们首先来看一看许多环 ...

  7. GO语言练习:channel select 超时机制

    1.代码 2.运行 3.解析 1.代码 package main import ( "time" "fmt" ) func waitFor(ch chan in ...

  8. GO语言系列(二)- 基本数据类型和操作符

    一.文件名 & 关键字 & 标识符 1.所有go源码以.go结尾 2.标识符以字母或下划线开头,大小写敏感 3._是特殊标识符,用来忽略结果 4.保留关键字 二.Go程序的基本结构 p ...

  9. 从头认识java-18.2 主要的线程机制(4)-优先级

    这一章节我们来讨论一下多线程的优先级问题. 1.样例: package com.ray.ch17; public class Test { public static void main(String ...

随机推荐

  1. CAD使用GetxDataLong读数据(com接口)

    主要用到函数说明: MxDrawEntity::GetxDataLong2 读取一个Long扩展数据,详细说明如下: 参数 说明 [in] LONG lItem 该值所在位置 [out, retval ...

  2. VC:UI编程

    VC++中给对话框设置背景图片的方法 -----------------------------------------------------方法一(铺满窗口)------------------- ...

  3. ORA-28000: the account is locked-详细解决方案

    运行-->cmd-->sqlplus /nolog conn system/orcl(或预设的密码) alter user scott identified by tiger(或预设密码) ...

  4. loader.js

    /** * @preserve Tiny-Loader: A small loader that load CSS/JS in best way for page performanceIs. * * ...

  5. 笔试算法题(39):Trie树(Trie Tree or Prefix Tree)

    议题:TRIE树 (Trie Tree or Prefix Tree): 分析: 又称字典树或者前缀树,一种用于快速检索的多叉树结构:英文字母的Trie树为26叉树,数字的Trie树为10叉树:All ...

  6. 创建Tensor

    目录 创建Tensor numpy, list numpy list zeros, ones, fill zeros ones fill random 打乱idx后,a和b的索引不变 constant ...

  7. Linux最常用的基础命令 上篇

    Linux最常用的基础命令个人总结 计算机基础知识 32bit和64bit系统的区别.系统运行机制 1989年python 诞生 C语言是编译型的语言,不太支持跨平台 Django 江购 32bit= ...

  8. Spring核心技术(七)——Spring容器的扩展

    本文将讨论如何关于在Spring生命周期中扩展Spring中的Bean功能. 容器的扩展 通常来说,开发者不需要通过继承ApplicationContext来实现自己的子类扩展功能.但是Spring ...

  9. 从Hadoop框架讨论大数据

    [Hadoop是什么?] 1)Hadoop 是一个由 Apache 基金会所开发的分布式系统基础架构. 2)主要解决,海量数据的存储和海量数据的分析计算问题. 3)广义上来说,HADOOP 通常是指一 ...

  10. Git和SVN共存的方法

    刚工作的时候都是用的cvs和svn,对git不熟悉,随着工作的需要,打分支和版本管理的需要,熟悉起来了git,这一用不可收拾,比svn远远好用,尤其是版本分支管理上,切换分支的方便性,现在这家公司还是 ...