赋值操作符的返回值
在进一步了解++a和a++的区别前,先来了解何谓操作符的计算(Evaluate)。操作符就是将给定的数字做一些处理,然后返回一个数字。而操作符的计算也就是执行操作符的处理,并返回值。前面已经知道,操作符是个符号,其一侧或两侧都可以接数字,也就是再接其他操作符,而又由于赋值操作符也属于一种操作符,因此操作符的执行顺序变得相当重要。
对于a + b + c,将先执行a + b,再执行( a + b ) + c的操作。你可能觉得没什么,那么如下,假设a之前为1:
c = ( a *= 2 ) + ( a += 3 );
上句执行后a为5。而c = ( a += 3 ) + ( a *= 2 );执行后,a就是8了。那么c呢?结果可能会大大的出乎你的意料。前者的c为10,而后者的c为16。
上面其实是一个障眼法,其中的“+”没有任何意义,即之所以会从左向右执行并不是因为“+”的缘故,而是因为( a *= 2 )和( a += 3 )的优先级相同,而按照“()”的计算顺序,是从左向右来计算的。但为什么c的值不是预想的2 + 5和4 + 8呢?因为赋值操作符的返回值的关系。
赋值操作符返回的数字不是变量的值,而是变量对应的地址。这很重要。前面说过,光写一个变量名就会返回相应变量的值,那是因为变量是一个映射,变量名就等同于一个地址。C++中将数字看作一个很特殊的操作符,即任何一个数字都是一个操作符。而地址就和长整型、单精度浮点数这类一样,是数字的一种类型。当一个数字是地址类型时,作为操作符,其没有要操作的数字,仅仅返回将此数字看作地址而标识的内存中的内容(用这个地址的类型来解释)。地址可以通过多种途径得到,如上面光写一个变量名就可以得到其对应的地址,而得到的地址的类型也就是相应的变量的类型。如果这句话不能理解,在看过下面的类型转换一节后应该就能了解了。
所以前面的c = ( a += 3 ) + ( a *= 2 );,由于“()”的参与改变了优先级而先执行了两个赋值操作符,然后两个赋值操作符都返回a的地址,然后计算“+”的值,分别计算两边的数字——a的地址(a的地址也是一个操作符),也就是已经执行过两次赋值操作的a的值,得8,故最后的c为16。而另一个也由于同样的原因使得c为10。
现在考虑操作符的计算顺序。当同时出现了几个优先级相同的操作符时,不同的操作符具有不同的计算顺序。前面的“()”以及“-”、“*”等这类二元操作符的计算顺序都是从左向右计算,而“!”、负号“-”等前面介绍过的一元操作符都是从右向左计算的,如:!-!!a;,假设a为3。先计算从左朝右数第三个“!”的值,导致计算a的地址的值,得3;然后逻辑取反得0,接着再计算第二个“!”的值,逻辑取反后得1,再计算负号“-”的值,得-1,最后计算第一个“!”的值,得0。
赋值操作符都是从右向左计算的,除了后缀“++”和后缀“—”(即上面的a++和a--)。因此上面的c = a = 3;,因为两个“=”优先级相同,从右向左计算,先计算a = 3的值,返回a对应的地址,然后计算返回的地址而得到值3,再计算c = ( a = 3 ),将3写入c。而不是从左向右计算,即先计算c = a,返回c的地址,然后再计算第二个“=”,将3写入c,这样a就没有被赋值而出现问题。又:
a = 1; c = 2; c *= a += 4;
由于“*=”和“+=”的优先级相同,从右向左计算先计算a += 4,得a为5,然后返回a的地址,再计算a的地址得a的值5,计算“*=”以使得c的值为10。
因此按照前面所说,++a将返回a的地址,而a++也因为是赋值操作符而必须返回一个地址,但很明显地不能是a的地址了,因此编译器将编写代码以从栈中分配一块和a同样大小的内存,并将a的值复制到这块临时内存中,然后返回这块临时内存的地址。由于这块临时内存是因为编译器的需要而分配的,与程序员完全没有关系,因此程序员是不应该也不能写这块临时内存的(因为编译器负责编译代码,如果程序员欲访问这块内存,编译器将报错),但可以读取它的值,这也是返回地址的主要目的。所以如下的语句没有问题:
( ++a ) = a += 34;
但( a++ ) = a += 34;就会在编译时报错,因为a++返回的地址所标识的内存只能由编译器负责处理,程序员只能获得其值而已。a++的意思是先返回a的值,也就是上面说的临时内存的地址,然后再将变量的值加一。如果同时出现多个a++,那么每个a++都需要分配一块临时内存(注意前面c = ( a += 3 ) + ( a *= 2 );的说明),那么将有点糟糕,而且a++的意思是先返回a的值,那么到底是什么时候的a的值呢?在VC中,当表达式中出现后缀“++”或后缀“—”时,只分配一块临时内存,然后所有的后缀“++”或后缀“—”都返回这个临时内存的地址,然后在所有的可以计算的其他操作符的值计算完毕后,再将对应变量的值写入到临时内存中,计算表达式的值,最后将对应变量的值加一或减一。
因此:a = 1; c = ( a++ ) + ( a++ );执行后,c的值为2,而a的值为3。而如下:
a = 1; b = 1; c = ( ++a ) + ( a++ ) + ( b *= a++ ) + ( a *= 2 ) + ( a *= a++ );
执行时,先分配临时内存,然后由于5个“()”,其计算顺序是从左向右,
计算++a的值,返回增一后的a的地址,a的值为2
计算a++的值,返回临时内存的地址,a的值仍为2
计算b *= a++中的a++,返回临时内存的地址,a的值仍为2
计算b *= a++中的“*=”,将a的值写入临时内存,计算得b的值为2,返回b的地址
计算a *= 2的值,返回a的地址,a的值为4
计算a *= a++中的a++,返回临时内存的地址,a的值仍为4
计算a *= a++中的“*=”,将a的值写入临时内存,返回a的地址,a的值为16
计算剩下的“+”,为了进行计算,将a的值写入临时内存,得值16 + 16 + 2 + 16 + 16为66,写入c中
计算三个a++欠下的加一,a最后变为19。
上面说了那么多,无非只是想告诫你——在表达式中运用赋值操作符是不被推崇的。因为其不符合平常的数学表达式的习惯,且计算顺序很容易搞混。如果有多个“++”操作符,最好还是将表达式分开,否则很容易导致错误的计算顺序而计算错误。