博学,切问,近思--詹子知 (https://jameszhan.github.io)
在网上看到网友发的帖子,对于程序 int j=2,m=2;m+=(j++)+(++j)+(j++); 执行后结果有争议, 甚至在不同的语言环境下,它们执行的结果也截然不同。
其实,同类型的问题有很多,最出名就数i=i++了,在Java中,无论执行多少次这样的语句,i的值都不会改变,而在C/C++中却能够顺利的自加。究其根源,这跟他们编译后生成的字节码或机器代码的方式有关。在Java中,它的执行过程可以等价于:
int j = i; i = i + 1; i = j;
而在C/C++中,自加操作需要等到整个语句执行完才执行,执行过程是这样的:
int j = i; i = j; i = i + 1;
对于第一个例子,在java环境中,本段程序执行后的结果是i=5,m=12,而在C中执行的结果却是i=5,m=11。这让我搞了几年Java的人很是诧异。Java的思维习惯比较符合我们的习惯,对于语句中每个表达式分别求值,然后把结果相加。执行过程等价于:
a = j++; b = ++j; c = j++; m += a + b + c;
最后的结果是:m = m + a + b + c = 2 + 2 + 4 + 4 = 12, 而 i自加了3次,最后的值为5。
在c/c++中,i++和++i在语句中的执行是有很大差异的,i++运算符的意义是执行完当前语句之后,将目标值加1,而++i是在执行语句之前先完成自加操作。所以其执行过程等价于:
j++; a = j; b = j; c = j; m += a + b + c; j++; j++;
最后的结果是: m = m + a + b + c = 2 + 3 + 3 + 3 = 11, i同样自加了3次,最后值为5。
对知识的理解一定不能只停留于表象,如果有可能,尽量去索本求源,事实上,这段程序汇编后的代码也验证了c语言的规则。
main: leal 4(%esp), %ecx andl $-16, %esp pushl -4(%ecx) pushl %ebp movl %esp, %ebp pushl %ecx subl $16, %esp movl $2, -12(%ebp) movl $2, -8(%ebp) addl $1, -8(%ebp) movl -8(%ebp), %eax addl -8(%ebp), %eax addl -8(%ebp), %eax addl %eax, -12(%ebp) addl $1, -8(%ebp) addl $1, -8(%ebp) addl $16, %esp popl %ecx popl %ebp leal -4(%ecx), %esp ret
为了更好的验证这个规则,我们可以写一段更复杂一点的语句,看看他汇编后的程序是什么样子。
int m, i = 2; m = (++i) + (--i) + (i++) + (++i) + (i--) + (++i);
这段程序,在Java中,m 的值是19,i 的值4. 执行的过程就是从前往后,每个表达式逐步执行,m=3+2+2+4+4+4 = 19。
而在C/C++中的执行过程是这样的,按照之前的规则,先做3次自加操作和一次自减操作,在把6个表达式的值相加,所以m=4+4+4+4+4+4=24,最后分别执行一次自加和自减操作。然而事实上,运行后的结果却是16。这又是为什么呢?我们先看一下,汇编后的代码。main: leal 4(%esp), %ecx andl $-16, %esp pushl -4(%ecx) pushl %ebp movl %esp, %ebp pushl %ecx subl $36, %esp movl $2, -12(%ebp) addl $1, -12(%ebp) subl $1, -12(%ebp) movl -12(%ebp), %eax addl -12(%ebp), %eax addl -12(%ebp), %eax addl $1, -12(%ebp) addl -12(%ebp), %eax addl -12(%ebp), %eax addl $1, -12(%ebp) addl -12(%ebp), %eax movl %eax, -8(%ebp) addl $1, -12(%ebp) subl $1, -12(%ebp)
用C代码还原是这样的:i++; i--; tmp += i; tmp += i; tmp += i; i++; tmp += i; tmp += i; i++; tmp += i; m = tmp; i++; i--; ,可以看出m=2+2+2+3+3+4=16。很显然,在求值的过程中,++i的操作和求整个表达式值的操作交替执行了,但是i--和i++的操作还是等到求值完成后再去做的。
可见,对于上述表达式的值,我们是很难预期的,即使同是C,很有可能在不同的编译器环境下得到的结果也是不同的。避免的方法只有一个,不要写这种容易引起混淆的代码,虽然多了几行代码,但是却可以让人看的更明白,代码不是写给计算机的,而是写给后来人看的,所以为了节省自己和他人的时间,请规范你自己的代码。