- C/C++中的情况
写这篇水文的缘由是技术群里的一个同学提出的问题,可能是什么笔试题目吧!内容很简单,求表达式(++a) + (++a) + (a++)的值,其中a=5。对于这个问题,估计大部分同学都会给出20这个答案,我也是这么想的。在Java中,这个表达式的答案也确实是20。可惜这个题目的背景是C/C++,问题好像不是那么简单了,VC6给出的答案是22。开始我没有仔细算,猜测是C/C++在处理算术表达式的时候使用了某种自顶向下的文法。为了消除左递归,表达式求值的时候是按照从右到左的顺序处理的。但是,这样也不能解释VC6给出的答案,这种求值顺序的答案应该是21才对。
理论分析吃瘪,我决定还是看看汇编吧!我先在GCC下做了个简单的实验,结果是21。反汇编的结果如下:
乍一看结果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的结果也是情理之中的。
一个看似非常简单的问题,却把我难倒了。研究一下,权当娱乐了!
发表评论