小试牛刀
//复杂表达式求值案例一 #include<stdio.h> int mian() { int i=1; int c= (++i) + (i++) + (i++); printf("C=%d",c); return 0; }
//复杂表达式求值案例二 int fun() { static int i=1; i++; return i; } #include<stdio.h> int main() { int sum=fun()+fun()+fun(); printf("sum=%d",sum); return 0; }
表达式求值
啥是表达式求值呢?
通俗的说就是,像我们加减乘除算数运算一样,通过计算求得运算结果,而C语言不止加减乘除运算,所有的C语言操作符,计算而得出结果,这就是表达式求值!
表达式求值的顺序
我们已经知道,表达式求值就是操作符运算的结果。像加减乘除都有自己的运算顺序,所以操作符都有自己的运算顺序。
而我们知道C语言有很多操作符,每个操作符的优先级,和结合性又不一样!
表达式求值的顺序一部分是由操作符的优先级和结合性决定。
同样,有些表达式的操作数在求值的过程中可能需要转换为其他类型。
隐式类型转换
什么是隐式类型类型转换
我们C语言中的数据类型很多,当不同类型的数据进行运算时,某一类数据,就会进行类型转换而后进行运算。
C语言中的隐式类型转换规则
C语言中的整型算术运算总是至少以缺省整型类型的精度来进行的。
为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种转换称为整型提升。
有人就该疑惑了,为啥C语言中要以整型的精度进行计算呢,为啥不能是浮点型?
整型提升的意义:
表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度一般就是int的字节长度,同时也是CPU的通用寄存器的长度。
因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长度。
通用CPU(general-purpose CPU)是难以直接实现两个8比特字节直接相加运算(虽然机器指令中可能有这种字节相加指令)。所以,表达式中各种长度可能小于int长度的整型值,都必须先转换为int或unsigned int,然后才能送入CPU去执行运算。
我们可以知道计算机中CPU中运算器的运算标准就是整型(int)进行运算,所以当数据类型不足一个整型,计算机会现将该数据进行整型提升,统一成整型,然后送入CPU执行运算!
我们来看看如何进行整型提升
//实例一 char a,b,c; a=3; //3 00000000 00000000 00000000 00000011 //a=3; 00000011 char 只能存8个二进制位 //整型3截断后放入a中 b=2; //2 00000000 00000000 00000000 00000010 //b=2; 00000010 char 只能存8个二进制位, //整型2截断后放入b中 c=a+b; //c=a+b;进行运算需要CPU运算器(ALU) //a和b不足一个整型,需要进行整型提升! // char a =3;提升后 //3 00000000 00000000 00000000 00000011 //char b=2;提升后 //2 00000000 00000000 00000000 00000010 //a+b //5 00000000 00000000 00000000 00000101 //将5存入char c中,进行截断 后 c 00000101 printf("c=%d",c);//最后进行%d(整型)打印时 //又将进行一次整型提升 5 //00000000 00000000 00000000 00000101
看到就短短一行代码,计算机却进行了这么多计算。这就是整型提升的步骤,看来表达式求值不易,计算机都这么麻烦,何况我们。
其实并不难的只要我们掌握了整型提升的规律,这都小菜一碟。
整型提升规则
整形提升是按照变量的数据类型符号位进行提升的
1.有符号数据类型,整型提升补符号位
2.无符号数据类型,整型提升补0
//实例二 char a = -1; //-1 原码:10000000 00000000 00000000 00000001 // 补码;11111111 11111111 11111111 11111111 // 截断a=-1 11111111 unsigned char b = 1; // 1 原码补码相同 // 00000000 00000000 00000000 00000001 //截断 b=1; 00000001 char c = a + b; // char a 11111111 有符号字符型 符号位提升 // 11111111 11111111 11111111 11111111 //unsigned char b 00000001 无符号字符型 补0提升 //00000000 00000000 00000000 00000001 //a+b //00000000 00000000 00000000 00000000 // char c 截断 00000000 printf("%d",c); //有符号的形式打印,进行整型提升后 //00000000 00000000 00000000 00000000
看到这里,你肯定会想,这提升了个锤子,这不就是,1+(-1)=0嘛还用得了这么麻烦?
我们看看下面代码你就知道了整型提升的意义
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 a=0xb6; // 0xb6 00000000 00000000 00000000 10110110 //截断放入a中 a 10110110 short b=0xb600; //0xb600 00000000 00000000 10110110 00000000 //截断放入b中 b 10110110 00000000 int c=0xb6000000; //00000000 00000000 00000000 0xb6000000 存入C中 if(a==0xb6) printf("a"); //a:10110110 符号位是1 提升 // 11111111 11111111 11111111 10110110 // 转换成原码: //00000000 00000000 00000000 01001010 //而0xb6 // 00000000 00000000 00000000 10110110 //显然a!=0xb6; if(b==0xb600) printf("b"); //b :10110110 00000000 符号位是1提升后 //11111111 11111111 10110110 00000000 //原码: //00000000 00000000 01001010 00000000 //而0xb600 //00000000 00000000 10110110 00000000 //b!=0xb600 if(c==0xb6000000) printf("c"); return 0;
整型提升要点总结:
计算机中是以数据的补码存储计算的,都是在补码的基础上继续整型提升和计算
整型提升补充的二进制位是要看该数据类型是有符号类型,还是无符号类型。
有符号类型补充符号位,也就是该数据的最左边的二进制位
无符号类型同一补充0
当一个数据进行有符号类型打印%d直接补充该数据的符号位,然后,将该数据转换成原码就是打印的结果
无符号打印时%u直接补充0然后该数据的原反补码相同。
算数转换
可能学会了整型提升的小伙伴就有所疑惑了,当一个数据超过整型呢?
当这些数据是long, long long , double 等数据类型计算时该如何呢?
如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类
型,否则操作就无法进行。下面的层次体系称为寻常算术转换。
该如何转换呢?
也是像整型提升那样,都转换成整型吗?
那必须不是
我们来看看算数转换的规律
//从下往上,层次转换 long double double float unsigned long int long int unsigned int int
如果某个操作数的类型在上面这个列表中排名较低,那么首先要转换为另外一个操作数的类型后执行运算!
许多操作数类型为算数类型的双目运算符会引发转化, 并以类似的方式产生结果类型。他的目的是产生一个普通类型, 同时也是运算结果的类型。这个模式称为“寻常算数转换”。 ——ANSI C手册
通俗来说:算数转换朝着精度更高,空间大小更大的类型进行转换。
//错误的转换 float f=3.1415; int a=f; //隐式转换,精度丢失!
操作符的属性
在C语言操作符详解中,我已经介绍过了C语言中的所有操作符,还没看的伙伴可以点击查看操作符详解!
复杂表达式的求值有三个影响的因素。
操作符的优先级
两个相邻操作符的计算顺序取决于它们的优先级。
操作符的结合性
当两个操作符的优先级相同时,计算顺序就要看它们的结合性,结合性就是决定从左向右,还是从右向左计算。
是否控制求值顺序。
控制求值顺序就是像||和&&一样会发生短路,当一个假时另一个表达式就停止计算,当一个为真时另一个表达式就停止计算!
两个相邻的操作符先执行哪个?取决于他们的优先级。如果两者的优先级相同,取决于他们的结合性。
操作符优先级及结合性
这是<<C和指针>>一书中操作符优先级表!
学习C语言的同学怎么可以没读过这本书,强烈安利大家学习!有需要电子版的伙伴可以私聊bug郭
操作符的优先级从上到下,由高到低!
表达式的求值部分由优先级决定!
问题表达式
//表达式一 a*b + c*d + e*f;
这个代码可能有多种结果
我们第一步只能确定在第一个乘和第一个加,是先计算a*b,而第三个乘不能确定是否比第一个加早,所以有多种计算方式。
//1 a*b c*d a*b+c*d e*f a*b+c*d+e*f //2 a*b c*d e*f a*b+c*d a*b+c*d+e*f
//表达式二 c + c--;
注释:同上,操作符的优先级只能决定自减--的运算在+的运算的前面,但是我们并没有办法得知,+操作符的左操作数的获取在右操作数之前还是之后求值,所以结果是不可预测的,是有歧义的。
//表达式三,非法表达式 int main() { int i = 10; i = i-- - --i * ( i = -3 ) * i++ + ++i; printf("i = %d\n", i); return 0; }
上面表达式的结果在不同编译器下结果不一样。
C和指针一书中整理了不同编译器下的结果
//表达式四 int fun() { static int count = 1; return ++count; } int main() { int answer; answer = fun() - fun() * fun(); printf( "%d\n", answer);//输出多少? return 0; }
这个代码有没有实际的问题?
有问题!
虽然在大多数的编译器上求得结果都是相同的。
但是上述代码answer = fun() - fun() * fun(); 中我们只能通过操作符的优先级得知:先算乘法,再算减法。函数的调用先后顺序无法通过操作符的优先级确定。
这样的表达式求值问题是压根没有意义的,如果学校再出此类问题,你就将<<C和指针>>一书甩在你老师面前!
//表达式五 #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编译器,VS2013环境下都执行,看结果。
linux环境gcc编译器结果
10
4
vs2013环境下结果
12
4
看看同样的代码产生了不同的结果,这是为什么?
简单看一下汇编代码,就可以分析清楚!
这我就不带大家研究了,如果有兴趣的伙伴可以研究一下!
这段代码中的第一个+ 在执行的时候,第三个++是否执行,这个是不确定的,因为依靠操作符的优先级和结合性是无法决定第一个+ 和第三个前置++ 的先后顺序。
总结:
我们写出的表达式如果不能通过操作符的属性确定唯一的计算路径,那这个表达式就是存在问题的!