《C和指针》笔记21:表达式求值

文章目录

  • 1. 隐式类型转换
  • 2. 算术转换
  • 3. 操作符的属性
  • 4. 优先级和求值顺序

1. 隐式类型转换

C的整型算术运算总是至少以缺省整型类型的精度来进行的。为了获得这个精度,表达式中的字符型和短整型操作数在使用之前被转换为普通整型,这种转换称为整型提升(integral Promotion)。例如,在下面表达式的求值中,

char a, b, c;
...
a = b + c;

b和c的值被提升为普通整型,然后再执行加法运算。加法运算的结果将被截短,然后再存储于a中。这个例子的结果和使用8位算术的结果是一样的。但在下面这个例子中,它的结果就不再相同。这个例子用于计算一系列字符的简单检验和。

a = ( ~a ^ b << 1 ) >> 1;

由于存在求补和左移操作,所以8位的精度是不够的。

2. 算术转换

如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数转换为另外一个操作数的类型,否则操作就无法进行。下面的层次体系称为寻常算术转换。

long double
double
float
unsigned long int
long int
unsigned int
int

如果某个操作数的类型在上面这个列表中排名较低,那么它首先将转换为另外一个操作数的类型然后执行操作。

int a = 5000;
int b = 25;
long c = a * b;

表达式a*b是以整型进行计算,在32位整数的机器上,这段代码运行起来毫无问题,但在16位整数的机器上,这个乘法运算会产生溢出,这样c就会被初始化为错误的值。解决方案是在执行乘法运算之前把其中一个(或两个)操作数转换为长整型

  • 当整型值转换为float型值时,也有可能损失精度。float型值仅要求6位数字的精度。如果将一个超过6位数字的整型值赋值给一个float型变量时,其结果可能只是该整型值的近似值。

  • 当float型值转换为整型值时,小数部分被舍弃(并不进行四舍五入)。如果浮点数的值过于庞大,无法容纳于整型值中,那么其结果将是未定义的。

3. 操作符的属性

下表列出了操作符的所有的属性,其中包括

  • 描述
  • 用法示例
  • 结果类型:lexp表示左值表达式,rexp表示右值表达式。

左值(可以实现在赋值符号左边的东西)意味着一个位置,右值(可以实现在赋值符号右边的东西)意味着一个值,所以使用右值的地方可以使用左值,但是需要左值的地方不能使用右值。

  • 结合性:从左往右写为L-R,从右往左写为R-L。没有结合性写作N/A。
  • 是否控制求值顺序:是/否。
操作符 描述 用法示例 结果类型 结合性 是否控制求值顺序
() 聚组 (表达式) 与表达式同 N/A
() 函数调用 rexp(rexp, ..., rexp) rexp L-R
[] 下标引用 rexp[rexp] lexp L-R
. 访问结构成员 lexp.member_name lexp L-R
-> 访问结构指针成员 rexp->member_name lexp L-R
++ 后缀自增 lexp++ rexp L-R
-- 后缀自增 lexp-- rexp L-R
! 逻辑反 !rexp rexp R-L
~ 按位取反 ~rexp rexp R-L
+ 单目,表示正值 +rexp rexp R-L
- 单目,表示负值 -rexp rexp R-L
++ 前缀自增 ++lexp rexp R-L
-- 前缀自减 --lexp rexp R-L
* 间接访问 *rexp lexp R-L
& 取地址 &lexp rexp R-L
sizeof 取其长度 sizeof rexp/sizeof(类型) rexp R-L
(类型) 类型转换 (类型)rexp rexp R-L
* 乘法 rexp * rexp rexp L-R
/ 除法 rexp / rexp rexp L-R
% 整数取余 rexp % rexp rexp L-R
+ 加法 rexp + rexp rexp L-R
- 减法 rexp - rexp rexp L-R
<< 左移位 rexp << rexp rexp L-R
>> 右移位 rexp >> rexp rexp L-R
> 大于 rexp > rexp rexp L-R
>= 大于等于 rexp >= rexp rexp L-R
< 小于 rexp < rexp rexp L-R
<= 小于等于 rexp <= rexp rexp L-R
== 等于 rexp == rexp rexp L-R
!= 不等于 rexp != rexp rexp L-R
& 位与 rexp & rexp rexp L-R
^ 位异或 rexp ^ rexp rexp L-R
| 位或 rexp | rexp rexp L-R
&& 逻辑与 rexp && rexp rexp L-R
|| 逻辑或 rexp || rexp rexp L-R
?: 条件操作符 rexp ? rexp : rexp rexp N/A
= 赋值 lexp = rexp rexp R-L
+= 以…加 lexp += rexp rexp R-L
-= 以…减 lexp -= rexp rexp R-L
*= 以…乘 lexp *= rexp rexp R-L
/= 以…除 lexp /= rexp rexp R-L
%= 以…取模 lexp %= rexp rexp R-L
<<= 以…左移 lexp <<= rexp rexp R-L
>>= 以…右移 lexp >>= rexp rexp R-L
&= 以…与 lexp &= rexp rexp R-L
^= 以…异或 lexp ^= rexp rexp R-L
|= 以…或 lexp |= rexp rexp R-L
, 逗号 rexp, rexp rexp L-R

4. 优先级和求值顺序

两个相邻操作符的执行顺序由它们的优先级决定。如果它们的优先级相同,它们的执行顺序由它们的结合性决定。除此之外,编译器可以自由决定使用任何顺序对表达式进行求值,只要它不违背逗号、&&||?:操作符所施加的限制。

对于表达式

a * b + c * d + e * f

其求值顺序可能是

a * b
c * d
(a * b) + (c * d)
e * f
(a * b) + (c * d) + (e * f)

也可能是

e * f
c * d
a * b
(a * b) + (c * d)
(a * b) + (c * d) + (e * f)

加法运算的结合性要求两个加法运算按照先左后右的顺序执行,但它对表达式剩余部分的执行顺序并未加以限制。尤其是,这里并没有任何规则要求所有的乘法运算首先进行,也没有规则规定这几个乘法运算之间谁先执行。优先级规则在这里起不到作用,优先级只对相邻操作符的执行顺序起作用。

由于表达式的求值顺序并非完全由操作符的优先级决定,所以像下面这样的语句是很危险的。

c + --c

--cc之前或之后执行,表达式的结果在两种情况下将会不同。像这样的表达式是不可移植的,应该予以避免。

编译器只要不违背优先级和结合性规则,它可以自由决定复杂表达式的求值顺序。表达式的结果如果依赖于求值的顺序,那么它在本质上就是不可移植的,应该避免使用。

不同的编译器可以以任何顺序对类似下面这样的表达式进行求值。

a + b + c
x * y * z

之所以允许编译器这样做是因为b+c(或y*z)的值可能可以从前面的一些表达式中获得,所以直接复用这个值比重新求值效率更高。加法运算和乘法运算都具有结合性,这样做的缺点在什么地方呢?

考虑下面这个表达式,它使用了有符号整型变量:

if( x + y + 1 > 0 )

如果表达式x+y的结果大于整型所能容纳的值,它就会产生溢出。在有些机器上,下面这个测试的结果将取决于先计算x+y还是y+1,因为在两种情况下溢出的地点不同。问题在于我们无法肯定地预测编译器将按哪种顺序对这个表达式求值。

下面的表达式也说明了这样一个问题:

f() + g() + h()

尽管左边那个加法运算必须在右边那个加法运算之前执行,但对于各个函数调用的顺序,并没有规则加以限制。如果它们的执行具有副作用,比如执行一些I/O任务或修改全局变量,那么函数调用顺序的不同可能会产生不同的结果。因此,如果顺序会导致结果产生区别,你最好使用临时变量,让每个函数调用都在单独的语句中进行。

temp = f();
temp += g();
temp += h();

其他注意的小点:

  1. 有符号值的右移位操作是不可移植的。
  2. 移位操作的位数是个负值。
  3. 使用复合赋值符可以使程序更易于维护。
  4. 使用条件操作符替代if语句以简化表达式。
  5. 使用逗号操作符来消除多余的代码。
  6. 不要混用整型和布尔型值。

相关参考
1.《C和指针》

你可能感兴趣的:(C和C++,c语言,表达式求值,隐式类型转换,算术转换,操作符属性,求值顺序,优先级)