表达式之运算顺序与求值顺序

  • C/C++中的情况

写这篇水文的缘由是技术群里的一个同学提出的问题,可能是什么笔试题目吧!内容很简单,求表达式(++a) + (++a) + (a++)的值,其中a=5。对于这个问题,估计大部分同学都会给出20这个答案,我也是这么想的。在Java中,这个表达式的答案也确实是20。可惜这个题目的背景是C/C++,问题好像不是那么简单了,VC6给出的答案是22。开始我没有仔细算,猜测是C/C++在处理算术表达式的时候使用了某种自顶向下的文法。为了消除左递归,表达式求值的时候是按照从右到左的顺序处理的。但是,这样也不能解释VC6给出的答案,这种求值顺序的答案应该是21才对。

理论分析吃瘪,我决定还是看看汇编吧!我先在GCC下做了个简单的实验,结果是21。反汇编的结果如下:

image

乍一看结果21,可能有人会认为这个结果的计算过程是6 + 7 + 8。到底是什么样,看汇编便知,对AT&T汇编不熟悉的同学可能会对lea  (%eax, %eax, 1), %ecx感到疑惑。这句汇编等价于mov  2*eax, ecx,即把一个值的两倍存放到ecx。所以,在GCC下得到的21,其实是通过7 + 7 + 7计算出来的。GCC为什么可以这么干?归根到底是因为C/C++标准中,并没有规定表达式的求值顺序。对于(exp1) + (exp2) + (exp3)这样的复杂表达式,根据运算符的结合性可以确定运算顺序。即一定是exp1和exp2先相加,两者之和再和exp3相加。但是对于三个子表达式的求值顺序,却没有一个确定的C/C++规范。其实,C/C++是很反对这种“作死”的写法,这种复杂的后效性表达式并没有什么很实际的需求。所以编译器对于这种“非标”写法,进行了自由发挥,不同编译器不太一样。

  • Java中情况

那么Java呢?首先,确定Java的结果是一致的(毕竟是一家独大的标准)。对于代码片段:

       int a = 5;
       a = (++a) + (++a) + (a++);

可以通过查看它的字节码,来理解Java的运算顺序和求值顺序,字节码文件如下:

       0: iconst_5
       1: istore_1
       2: iinc          1, 1
       5: iload_1
       6: iinc          1, 1
       9: iload_1
      10: iadd
      11: iload_1
      12: iinc          1, 1
      15: iadd
      16: istore_1

坦诚的讲,我对Java字节码也挺生疏的,毕竟已经很久没有看过JVM了。所以,为了看这段代码,我稍稍地复习了一下字节码。可喜的是,这段字节码非常简单,所以我们并不需要花费很多功夫。这段字节码文件中出现的字节码列在下表中,方便阅读。

字节码 堆栈变化 指令描述
iconst_ =>n 把常量n加载到栈上
istore_ value=> 把栈顶变量弹出到局部变量n
iinc No change 把一个局部变量自增
iload_ =>value 把局部变量n入栈
iadd value1,value2=>result 弹出栈顶两变量求和并入栈

有了上表,前面的字节码片段就非常简单了。我们可以发现,Java的求值顺序也是严格的从左到右,得出20的结果也是情理之中的。

一个看似非常简单的问题,却把我难倒了。研究一下,权当娱乐了!

发表评论