C和指针 5.4表达式求值

缘起

《C和指针》 5.4节

分析

隐式类型转换

C的整型算术运算总是至少以int(普通整型)为精度进行的. 为了获得这个精度,表达式中的字符型和短整型在使用之前都需要转换为普通整型int. 这个转换被称为是提升. 关于提升和截断,参见5.1节的getchar的例子

算术转换

从 高到低排序

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

如果某个操作数在这个排序中较低,则和较高的操作数进行运算的时候要先转换为另一个操作数的类型之后才能进行运算.

下面的代码可能是有问题的

1
2
3
int a = 5000;
int b = 25;
long c = a*b;

在32位整数的机器上,这段代码运行起来毫无问题,但是16位整数的机器上,乘法会溢出. 所以需要写成

1
long c = (long)a*b;

float转换为整型的话,小数部分被截掉.

表达式的求值顺序由3个因素决定

  1. 操作符的优先级

    操作符的优先级仅仅决定相邻2个操作符的执行顺序 ,如果相邻操作符的优先级一样,则考虑它们的结合性.

  2. 操作符的结合性

    结合性就是一串操作符是从左至右还是从右至左逐个执行.

  3. 操作符是否控制执行的顺序

    有4种操作符——逗号、&&、||、?:

    它们可以对整个表达式的求值顺序加以控制.它们或者保证了某个子表达式能够在另一个子表达式的所有求值完成之前进行求值(逗号表达式),或者有某种短路性(其余三个,尤其是三目运算符?:,如果布尔表达式为真,则只会求第一个,另一个不会求,结果是第一个表达式的值,反之,则只会求第二个,第一个不会求,结果是第二个表达式的值).

举个例子, 你就会知道这些规则并不平庸

1
a*b+c*d+e*f;

这个表达式既可以按照下面的顺序执行

1
2
3
4
a*b;
c*d;
e*f;
(a*b)+(c*d)+(e*f);

也可以按照下面的顺序执行(因为第一个加号和最后一个*号不是相邻的, 所以并不能要求编译器一定先进行第三个乘法才能进行第一个加法)

1
2
3
4
5
a*b;
c*d;
(a*b)+(c*d);
e*f;
(a*b)+(c*d)+(e*f);

因为上面两种顺序都满足 + 的结合性,也满足 +和*的优先级.

上面的求值顺序的不同并不会造成什么影响. 但是看看下面的表达式

1
c+--c;

这种表达式就不具备可移植性. 因为–和+的优先级规定了–要在+之前进行,但是C标准并没有规定参与+的两个表达式求值的顺序, 如果先对+的第一个操作数求值, 比如c=12, 则参与+的第一个表达式的值就是12,第二个参与+的表达式的值就是11, 则结果就是23. 如果先对+右侧的表达式进行求值呢? –c的结果是11,则再对+左侧的表达式进行求值的话, 就是11,则结果就是22. 所以这个表达式在不同编译器上运行的结果是不一样的. 所以该表达式不具备可移植性(有歧义),需要避免的.

类似的例子还有 KR C标准中,编译器可以自由决定(而不需要考虑结合性规则)以任何顺序对下面的表达式进行求值

1
2
a+b+c;
x*y*z;

例如,可以先求b+c, 再求 a+(b+c). 因为可能前面求过b+c, 则可以直接复用缓存. 但是这样会有一些问题

1
2
3
4
5
6
7
if(x+y+1>0)

{

...

}

如果x+y会溢出的话,则无法决定溢出的地点.

所以ANSI C不允许无视操作符的结合性.

类似的例子还有如下表达式

1
f()+g()+h();

+的结合性决定了是从左至右做加法. 即是如下的顺序.

1
2
f()+g();
(f()+g())+h();

但是和上面c+–c类似的,并没有规定, 是先求f还是先求g, 如果f和g有副作用——例如会修改全局变量的话,则会影响h, 也就是上面的表达式不具备可移植性. 应该避免.