我们写出的表达式,在求值的过程中,一定是按照我们所想的在一步一步运算吗?会不会发生一些我们察觉不到的变化呢?任意给定一个表达式,它的计算路径一定是确定的吗?
这篇文章,将带领大家深入的讨论和学习这些问题,一起来看看吧!
表达式求值
表达式求值的顺序一部分是由操作符的优先级和结合性决定。
同时,有些表达式的操作数在求值的过程中可能需要转换为其他类型。
一.隐式类型转换——整型提升
1.什么是整型提升呢?
C的整型算术运算总是至少以缺省整型类型的精度来进行的。
为了获得这个精度,表达式中的字符和短整型(所占空间大小小于一个整型的大小)操作数在使用之前被转换为普通整型,这种转换称为整型提升。
比如:
char a,b,c;
…
a = b + c;
这里就会发生整型提升:
(1) b和c的值被提升为普通整型,然后再执行加法运算。
(2) 加法运算完成之后,结果将被截断,然后再存储于a中。
2.那如何进行整型提升呢?按照什么规则?
整形提升是按照变量的数据类型的符号位来提升的:
(1)负数的整形提升(高位补符号位1)
char c1 = -1;
变量c1的二进制位(补码)中只有8个比特位:
1111111
因为 char 为有符号的 char
所以整形提升的时候,高位补充符号位,即为1
提升之后的结果是:
11111111111111111111111111111111
(2)正数的整形提升(高位补符号位0)
char c2 = 1;
变量c2的二进制位(补码)中只有8个比特位:
00000001
因为 char 为有符号的 char
所以整形提升的时候,高位补充符号位,即为0
提升之后的结果是:
00000000000000000000000000000001
(3)无符号整形提升,高位补0
unsigned char c1 = -1;
变量c1的二进制位(补码)中只有8个比特位:
1111111
因为 char 为无符号的 char
所以整形提升的时候,高位补0
提升之后的结果是:
00000000000000000000000011111111
3.实战演练
好,了解了规则之后,我们就来举几个例子吧。
实例一:
#include <stdio.h> int main() { unsigned char a = -1; short b = 1; printf("%d", a + b); return 0; }
大家思考思考,会不会发生整型提升,结果是啥,是-1+1=0吗?
好,我们一起来分析一下:
根据我们的分析答案是256。我看运行看看是不是:
实例二:
int main() { char a = 0xb6; short b = 0xb600; int c = 0xb6000000; if (a == 0xb6) printf("a"); if (b == 0xb600) printf("b"); if (c == 0xb6000000) printf("c"); return 0; }
这道题的结果又是啥呢?
看看结果:
只打印了c。
实例三:
int main() { char c = 1; printf("%u\n", sizeof(c)); printf("%u\n", sizeof(+c)); printf("%u\n", sizeof(-c)); return 0; }
我们分析结果应该是 1,4,4;
c只要参与表达式运算,就会发生整形提升,表达式 +c ,发生整型提升,所以 sizeof(+c) 是4个字节(一个整型大小).
同理 sizeof(-c) 也是4个字节,
但是 sizeof( c ) ,就是1个字节,因为没有发生整型提升,还是char类型。
二.算术转换
1.什么是算术转换
然后我们来学习算术转换,那什么是算术转换呢?
算术转换:
如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类型,否则操作就无法进行。
下面的层次体系称为寻常算术转换。
2.举例
#include <stdio.h> int main() { int a = 10; double b = 22.2; printf("%d", sizeof(a + b)); return 0; }
这段代码会不会发生算术转换,结果是什么呢?
我们来分析一下:
我们看看结果:
三.操作符的属性
1.操作符如何控制表达式求值
复杂表达式的求值有三个影响的因素。
1. 操作符的优先级
2. 操作符的结合性
3. 是否控制求值顺序
那它们如何取影响表达式的求值顺序呢?
(1)两个相邻的操作符先执行哪个?取决于他们的优先级。
(2)如果两者的优先级相同,取决于他们的结合性。
注意:N/A是空的意思,R/L是从右向左的意思,L/R是从左向右的意思。
举例说明一下:
如果优先级相同:
然后给大家解释一下控制求值顺序是什么意思吧!
举个例子,我们看到上面表格中的 rexp1? rexp2:rexp3 是控制求值顺序的。
其实就是rexp1可以决定rexp2,rexp3,哪一个表达式先算,哪一个后算.
因为我们知道他的运算规律是:
如果rexp1成立,执行rexp2,整个表达式的结果是rexp2的结果;
如果rexp1不成立,执行rexp3,整个表达式的结果是rexp3的结果
所以说它可以控制求值顺序。
2.问题表达式
接下来我们一起来看一些表达式,大家判断一下它们的求值顺序:
a*b + c*d + e*f
1
这其实是一个问题表达式,为啥呢?
在计算的时候,由于“ * ”比+的优先级高,只能保证 “ * ”的计算是比+早,但是优先级并不能决定第三个*比第一个+早执行。
所以表达式的计算机顺序就可能是:
ab
cd
ab + cd
ef
ab + cd + ef
或者
ab
cd
ef
ab + cd
ab + cd + ef
再来看一个:
c + --c;
同样存在一些问题:
同上,操作符的优先级只能决定自减- -的运算在+的运算的前面,但是我们并没有办法得知,+操作符的左操作数的值的获取是在右操作数- -之前获取的还是之后获取,所以结果是不可预测的,是有歧义的。
比如:
在不同的编译器上,结果就可能不一致!!!
再来一个:
int main() { int i = 10; i = i-- - --i * ( i = -3 ) * i++ + ++i; printf("i = %d\n", i); return 0; }
代码3在不同编译器中的测试结果:
值 编译器
—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
#include <stdio.h> int main() { int i = 1; int ret = (++i) + (++i) + (++i); printf("%d\n", ret); printf("%d\n", i); return 0; }
这段代码中的第一个 + 在执行的时候,第三个++是否执行,这个是不确定的,因为依靠操作符的优先级和结合性是无法决定第一个 + 和第三个前置 ++ 的先后顺序。
尝试在linux 环境gcc编译器,VS环境下都执行,看结果。
vs环境下:
这些都是有问题的表达式,我们在写代码的过程中,要避免写这样的代码!!!
3.总结
上面的问题表达式告诉我们:
即使我们知道了所有操作符的优先级和结合性,以及其是否控制求值顺序,我们也不能保证任意写一个表达式,它的求值顺序就一定是确定的,我们写出的表达式如果不能通过操作符的属性确定唯一的计算路径,那这个表达式就是存在问题的。
所以,对于比较复杂的代码我们可以拆开写,养成良好的代码风格,避免写出这样不好的代码!!!
以上就是对表达式求值的一些问题讲解,欢迎大家指正!!!