一、左值与右值
为了理解有些操作符存在的限制,必须理解左值(L-value)和右值(R-value)之间的区别。这两个术语是多年前由编译器设计者所创造并沿用至今,尽管它们的定义并不与C语言严格吻合。
左值就是那些能出现在赋值符号左边的东西。右值就是那些可以出现在赋值符号右边的东西。举栗:
a = b + 25;
a是个左值,因为它标识了一个可以存储结果值的地点,b+25是个右值,因为它指定了一个值。
它们可以互换吗?
b + 25 = a;
原先作左值的a此时也可以当做右值,因为a的位置也包含一个值。然而,b+25不能作为左值。因为它并未标识一个特定的位置。因此这条赋值语句是非法的。
当计算b+25时,它的结果必然保存于机器的某个地方。但是,程序员没办法预测该结果会存储于什么地方,也无法保证这个表达式的值下次还会存储于那个地方。其结果是,这个表达式不是一个左值。基于同样的理由,字面值常量也都不是左值。
听上去似乎变量可以作为左值而表达式不能作为左值,但是这个推断并不准确。在下面的赋值语句中,左值便是一个表达式。
int a[30];
...
a[b + 10] = 0;
下标引用实际上是一个操作符,所以表达式的左边实际上是个表达式,但它却是一个合法的左值,因为它标识了一个特定的位置,我们以后还能再程序中引用它。这里再举一个栗子:
int a, *p;
...
p = &a;
*p = 20;
第二条赋值语句,它左边的那个值显然是一个表达式,但它也是个合法的左值。因为指针p的值是内存中某个特定的位置的地址,*操作符使机器指向那个位置,当它作为左值使用时,这个表达式指定需要进行修改的位置。当它作为右值使用时,它就提取了当前存储于这个位置的值。
有些操作符,如间接访问(*)和下标引用([]),它们的结果是个左值。其余操作符的结果则是个右值。
二、表达式求值
表达式的求值顺序一部分是由它所包含的操作符的优先级和结合性决定。同样有些表达式的操作数在求值过程中可能转换为其他类型。
1.隐式类型转换
C的整型算术运算总是至少以缺省整型类型(default type)的精度来进行的。为了获得这个精度,表达式中的字符型和短整型操作数在使用之前被转换为普通整型,这种转换称为整型提升(integral promotion)。例如,
char a, b, c;
...
a = b + c;
b和c的值都被提升为普通整型,然后执行加法运算。加法运算的结果将被截短,然后再存储于a中。这个例子的结果和使用8位算术的结果是一样的。但在下面例子中,它的结果就不再相同。这个例子用于计算一系列字符的简单检验和。
a = (~a ^ b << 1) >> 1;
由于存在求补和左移操作,所以8位精度是不够的。标准要求进行完整的整型求值,所以对于这类表达式的结果,不会存在歧义性。(标准说明结果应该是通过完整的整型求值得到,编译器如果知道采用8位精度的求值不会影响最终结果,它也允许编译器这样做)。
2.算术转换
如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数转换为另外一个操作数的类型,否则操作就无法进行。下面的层次体系称为寻常算术转换(usual arithmetic conversion)。
long double
double
float
unsigned long int
long int
unsigned int
int
如果某个操作数的类型在上面排名较低,那么它首先将转换为另外一个操作数的类型然后执行操作。
3.操作符的属性
复杂表达式的求值顺序是由3个因素决定的:操作符的优先级、操作符的结合性及操作符是否控制执行的顺序。相邻两个操作符哪个县执行取决于它们的优先级,如果两者的优先级相同,那么它们的执行顺序由它们的结合性决定。简单地说,结合性就是一串操作符是从左向右依次执行还是反过来执行。最后,有4个操作符,它们可以对整个表达式的求值顺序施加控制,它们能保证某个表达式能够在另一个子表达式的所有求值过程完成之前进行求值,也可能使某个表达式被跳过不再求值。
每个操作符的所有属性都列在下优先级表中。表中各个列分别代表操作符、它的功能简述、用法示例、结果类型、结合性以及当它出现时是否会对表达式的求值顺序施加控制。用法示例提示它是否需要操作数为左值。术语lexp(left-expression)表示左值表达式,rexp表示右值表达式。记住,左值意味着一个位置,而右值意味着一个值。所以,在使用右值的地方也可以使用左值。但是需要左值的地方不能使用右值。
4.优先级和求值的顺序
如果一个表达式中的操作符超过一个,是什么决定这些操作符的执行顺序呢?答案是优先级,C的每个操作符都有其优先级,用于确定它和表达式中其余操作符之间的关系。但仅凭优先级还不能确定求值的顺序。下面是它的基本规则:
两个相邻的操作符的执行顺序由它们的优先级决定。如果它们的优先级相同,它们的执行顺序由它们的结合性决定。除此之外,编译器可以自由决定使用任何顺序对表达式进行求值,只要它不违背逗号、&&、||和?:操作符所施加的限制。
换句话说,表达式中操作符的优先级只决定表达式的各个组成部分在求值过程中如何进行聚组。举个例子:
a + b * c
在这个表达式中,乘法和加法操作符是两个相邻的操作符。由于*操作符的优先级比+操作符高,所以乘法运算先于加法运算执行。编译器在这里别无选择,它必须先执行乘法运算。
下面是一个更为有趣的表达式:
a * b + c * d + e * f
如果仅由优先级决定这个表达式的求值顺序,那么所有三个乘法运算将在所有加法运算之前进行。事实上,这个顺序并不是必需的。实际上只要保证每个乘法运算在它相邻的加法运算之前进行即可。例如,这个表达式可能会以下面的顺序进行,其中粗体的操作符表示在每个步骤中进行操作的操作符。
a * b
c * d
(a*b) + (c*d)
e * f
((a*b)+(c*d)) + (e*f)
注意第一个加法运算在最后一个乘法运算之前进行。如果这个表达式按以下顺序执行,其结果是一样的。
c * d
e * f
a * b
(a*b) + (c*d)
((a*b)+(c*d)) + (e*f)
加法运算的结合性要求两个加法运算按照先左后右的顺序执行,但它对表达式剩余部分的执行顺序并未加以限制。尤其是,这里并没有任何规则要求所有的乘法运算首先执行,也没有规则规定这几个乘法运算之间谁先执行。优先级规则在这里起不到作用,优先级只对相邻操作符的执行顺序起作用。
警告:
由于表达式的求值顺序并非完全由操作符的优先级决定,所以像下面这样的语句是很危险的。
c + --c
操作符的优先级规则要求自减运算在加法运算之前进行,但我们并没有办法得知加法操作符的左操作数是在右操作数之前还是之后进行求值。它在这个表达式中将存在区别(二义性),因为自减操作符具有副作用。--c和c之前或之后执行,表达式的结果在两种情况下将会不同。
标准说明类似这种表达式的值是未定义的。尽管每种编译器都会为这个表达式产生某个值,但到底哪个是正确的并无标准答案。因此,这样的表达式应该避免。
同样的,下面这个表达式说明了一个相关的问题。
f() + g() + h()
尽管左边那个加法运算必须在右边那个加法运算之前执行,但对于各个函数调用的顺序,并没有规则加以限制。如果它们的执行具有副作用,比如执行一些I/0任务或修改全局变量,那么函数的调用顺序的不同可能会产生不同的结果。因此,如果顺序导致结果产生区别,你做好使用临时变量,让每个函数调用都在单独的语句中进行。
temp = f();
temp += g();
temp += h();
总结
C具有丰富的操作符。算术操作符包括+(加)、-(减)、*(乘)、/(除)、%(取模)。除了%之外,其余的操作符不仅可以作用于整型还可以作用于浮点型。
<<和>>操作符分别执行左移位和右移位操作。&、|和^分别执行与、或和异或操作。这几个操作符都要求其操作数为整型。
=操作符执行赋值操作。而且,C还存在复合赋值符,它将赋值符和前面的操作符结合在一起:
+= -= *= /= %=
<<= >>= &= ^= |=
复合赋值符在做右操作数之间执行指定的运算,然后把结果赋值给左操作数。
单目运算符包括!(逻辑非)、~(按位取反)、-(负)、+(正)、++和--操作符分别用于增加或减少操作数的值。这两个操作数有前缀和后缀形式。前缀形式在操作数的值被修改之后才返回这个值,而后缀形式在操作数的值被修改之前就返回这个值。&操作符返回一个指向它的操作数指针(取地址),而*操作符对它的操作数(必须为指针)进行间接访问操作。sizeof返回操作数的类型的长度,以字节为单位。最后,强制类型转换(cast)用于修改操作数的数据类型。
关系操作符有:
> >= < <= != ==
每个操作符根据它的操作数之间是否存在指定的关系,或返回真,或返回假。逻辑操作符用于计算复杂的布尔表达式。对于&&操作符,只有当它的两个操作数都为真时,它的值才是真;||操作符,只有它的两个操作数都为假时,它的值才是假。这两个操作符会对包含它们的表达式的求值过程施加控制。如果整个表达式的值通过左操作数便可确定,那么右操作数便不再求值。
条件操作符?:接受三个参数,它也会对表达式的求值过程施加控制。如果第一个操作数的值为真,那么整个表达式的结果就是第二个操作数的值,第三个操作数不会执行。否则结果是第三个操作数的值,第二个操作数不会执行。逗号操作符把两个或更多的表达式连接在一起,从左向右一次进行求值,整个表达式的值就是最右边的子表达式的值。
左值是个表达式,它可以出现在赋值符的左边,它表示计算机内存中的一个位置。右值表示一个值,所以它只能出现在赋值符的右边。每个左值表达式同时也是个右值,但反过来就不是这样。
各个不同类型之间不能直接进行运算,除非其中之一的操作数转换为另一操作数的类型。寻常算术转换决定哪个操作数将被转换。操作符的优先级决定了相邻的操作符哪个先被执行。如果它们的优先级相等,那么它们的结合性将决定它们执行的顺序。但是,这些并不能完全决定表达式的求值顺序。编译器只要不违背优先级和结合性规则,就可以自由决定复杂表达式的求值顺序。表达式的结果如果依赖于求值的顺序,那么它在本质上是不可移植的,应避免使用。