我在前一篇文章中介绍了各种操作符,接下来让我们继续来了解表达式是如何进行求值的。
表达式求值主要涉及两个板块:数据类型转换和计算顺序。
目录
数据类型转换
隐式类型转换(整型提升)
C的整型算术运算总是至少以缺省整型类型(int)的精度来进行的。因为表达式的整型运算要在CPU的相应运算器件内执行,而CPU内整型运算器(ALU)的操作数的字节长度一般就是int的字节长度,同时也是CPU的通用寄存器的长度。
那么为了获得这个精度,表达式中的字符(char)和短整型(short)操作数在使用之前被转换为普通整型(int),这种转换称为整型提升。
如何实现整型提升:整形提升是按照变量的数据类型的符号位来提升的,即负数高位补1,正数高位补0,无符号数也是高位补0
下面举例:
负数的整形提升
char c1 = -1;
变量c1的二进制位(补码)中只有8个比特位:
1111111
因为 char 为有符号的 char
所以整形提升的时候,高位补充符号位,即为1
提升之后的结果是:
11111111111111111111111111111111
正数的整形提升
char c2 = 1;
变量c2的二进制位(补码)中只有8个比特位:
00000001
因为 char 为有符号的 char
所以整形提升的时候,高位补充符号位,即为0
提升之后的结果是:
00000000000000000000000000000001
如何看出char和short类型的确发生了整型提升呢?可以看看下面这段代码:
int main()
{
char a = 0xb6;//0x是指16进制
short b = 0xb600;
int c = 0xb6000000;
if(a==0xb6)
printf("a");
if(b==0xb600)
printf("b");
if(c==0xb6000000)
printf("c");
return 0;
}
可以看到,这里只输出了c,为什么呢?下面我用a举例浅浅分析一下
char a = 0xb6
变量a的二进制位(补码)中只有8个比特位:
10110110提升之后的结果是:
11111111111111111111111110110110
而0xb6实际上为
00000000000000000000000010110110
所以a != 0xb6
算术转换
如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类型,否则操作就无法进行。下面的层次体系称为寻常算术转换。
如果某个操作数的类型在上面这个列表中排名较低,那么首先要转换为另外一个操作数的类型后执行运算。
注意: 算术转换要合理,要不然会有一些潜在的问题。(可能会精度丢失)
计算顺序
复杂表达式的求值有三个影响的因素:
1. 操作符的优先级
2. 操作符的结合性
3. 是否控制求值顺序
两个相邻的操作符如何执行?取决于他们的优先级。如果两者的优先级相同,取决于他们的结合性
下面有个表格进行总结(结合性N/A是不判断,L-R是从左往右,R-L是从右往左)
操作符 | 描述 | 用法示例 | 结果类型 | 结合性 | 是否控制求值顺序 |
() | 聚组 | (表达式) | 与表达式同 | 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 | 是 |
这里要注意,如果我们写出的表达式如果不能通过操作符的属性确定唯一的计算路径,那这个表达式就是存在问题的。
不然就会出现这样的情况
#include<stdio.h>
int main()
{
int i = 10;
i = i-- - --i * ( i = -3 ) * i++ + ++i;
printf("i = %d\n", i);
return 0;
}
这个代码在不同的编译器下出现了这样一些结果
值 | 编译器 |
—128 | Tandy 6000 Xenix 3.2 |
—95 | Think C 5.02(Macintosh) |
—86 | IBM PowerPC AIX 3.2.5 |
—85 | Sun Sparc cc(K&C编译器) |
—63 | gcc,HP_UX 9.0,Power C 2.0.0 |
4 | Sun Sparc acc(K&C编译器) |
21 | Turbo C/C++ 4.5 |
22 | FreeBSD 2.1 R |
30 | Dec Alpha OSF1 2.0 |
36 | Dec VAX/VMS |
42 | Microsoft C 5.1 |
可见要写出好代码,我们写出的表达式必须能通过操作符的属性确定唯一的计算路径