前言
通过上期的讲解,相信大家对操作符已经有了一定的理解,其实学习操作符是为了筑基,学习操作符的根本目的是为了表达式求值,本章就以此目的继续探讨表达式求值!lets go!
一、隐式类型转换
在表达式求值时,有些表达式的操作数在求值的过程中可能需要转换为其他类型,其主要体现为整型提升和算术转换。
1、整型提升
(1)什么是整型提升?
C的整型算术运算总是至少以默认整型类型的精度来进行的。为了获得这个精度,表达式中的char-字符型和short-短整型操作数在使用之前被转换为普通整型,这种转换称为整型提升。
整形提升的条件是:表达式中大小达不到int的char
和short
才会发生整型提升
(2)为什么会发生整型提升?
表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度一般就是int的字节长度,同时也是CPU的通用寄存器的长度。因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长 度。
通用CPU(general-purpose CPU)是难以直接实现两个8比特字节直接相加运算(虽然机器指令中可能有这种字节相加指令)。所以,表达式中各种长度可能小于int长度的整型值,都必须先转 换为int或unsigned int,然后才能送入CPU去执行运算。
(3)如何进行整型提升?
整型提升是按照变量的数据类型的符号位来提升的。对于有符号整型提升高位
补符号位
;对于无符号整型提升,高位补0
📝例如:计算并打印 a+b
#include<stdio.h> int main() { char a = 5;//截断 //00000000000000000000000000000101 //00000101 - a char b = 126;//截断 //00000000000000000000000001111110 //01111110 - b char c = a + b;//整型提升+截断 //整型提升 //00000000000000000000000000000101-a //00000000000000000000000001111110-b //00000000000000000000000010000011 //10000011 - c printf("%d\n", c); //%d 十进制的方式打印有符号整数,对c整型提升 //11111111111111111111111110000011 //11111111111111111111111110000010 //10000000000000000000000001111101 //-125 return 0; //输出:-125 }
(4)小试牛刀
📝1、下面代码输出的结果是什么?
#include<stdio.h> 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; }
📝2、下面代码输出结果是什么?
#include<stdio.h> int main() { //sizeof返回值是无符号整型,可以用u%打印 char c = 1; printf("%u\n", sizeof(c)); printf("%u\n", sizeof(+c)); printf("%u\n", sizeof(-c)); return 0; } //输出:1 4 4
为什么程序输出结果存在4 呢?正是因为只要char或short存在于表达式中,即使不计算也会发生整型提升,因此会输出4
2、算术转换
(1)寻常算术转换
如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类
型,否则操作就无法进行。
转换条件:表达式中存在不同类型,并且大于等于int
下面的层次体系称为寻常算术转换:👇
注意:有些转换可能会丢失精度
如:
float f = 3.14;
int num = f;
(2)一道变态的算术转换题
📝下面程序输出结果是什么?
#include <stdio.h> int i; int main() { i--; if (i > sizeof(i)) { printf(">\n"); } else { printf("<\n"); } return 0; }
解析:C语言中,0为假,非0即为真。全局变量,没有给初始值时,编译其会默认将其初始化为0。i的初始值为0,i- - 结果-1,i为整形,sizeof(i)求i类型大小是4,按照此分析来看,输出结果为<,但是sizeof的返回值类型实际为无符号整形,因此编译器会自动将左侧i自动转换为无符号整形的数据(算术转换),-1对应的无符号整形是一个非常大的数字,超过4或者8,故输出结果为>这道题其实很隐蔽,真是虾仁猪心!!!
二、操作符的属性
1、操作符的三种属性
复杂表达式的求值有三个属性,也就是三个影响的因素:
- 操作符的优先级
- 操作符的结合性
- 是否控制求值顺序。
两个相邻的操作符先执行哪个?取决于他们的优先级。如果两者的优先级相同,取决于他们的结合性。
2、操作符属性表
注:
从上往下操作符的优先级越来越低。优先级最高位()
最低为,
- N/A表示不具有结合性;
L-R
从左向右结合;R-L
从右向左结合。 - 只有逻辑与-
&&
、逻辑或-||
条件操作符-?:
逗号-,
操作符控制求值顺序。
补充:控制求值顺序是指,这些操作符在表达式求值时会决定哪些值计算哪些值不计算。
操作符 | 描述 | 用法用例 | 结果类型 | 结合性 | 是否控制求值顺序 |
() | 聚组 | (表达式) | 与表达式相同 | N/A | 否 |
() | 函数调用 | repx(repx,…,repx) | repx | L-R | 否 |
[ ] | 下标引用 | repx[repx] | 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 | 否 |
++ | 前缀自增 | ++rexp | rexp | R-L | 否 |
- - | 前缀自减 | - -rexp | 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 a, b, c; a = 5; c = ++a;// ++a:加给a+1,结果为6,用加完之后的结果给c赋值,因此:a = 6 c = 6 b = ++c, c++, ++a, a++; // 逗号表达式的优先级,最低,这里先算b=++c, b得到的是++c后的结果,b是7 // b=++c 和后边的构成逗号表达式,依次从左向右计算的。 // 表达式结束时,c++和,++a,a++会给a+2,给c加1,此时c:8,a:8,b:7 b += a++ + c; // a先和c加,结果为16,在加上b的值7,比的结果为23,最后给a加1,a的值为9 printf("a = %d b = %d c = %d\n:", a, b, c); // a:9, b:23, c:8 return 0; } //输出:a=9 b=23 c=8
3、问题表达式
通过操作符的属性已经可以解决绝大多数的表达式求值,但是如果操作符使用不当很可能造成问题表达式。
📝问题表达式1
a*b + c*d + e*f
注释:代码1在计算的时候,由于*
比+
的优先级高,只能保证,*
的计算是比+
早,但是优先级并不能决定第三个*比第一个+早执行。
📝问题表达式2
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();
中我们只能通过操作符的优先级得知:先算乘法,再算减法。函数的调用先后顺序无法通过操作符的优先级确定。
📝问题表达式3
#include <stdio.h> int main() { int i = 1; int ret = (++i) + (++i) + (++i); printf("%d\n", ret); printf("%d\n", i); return 0; }
注释:这段代码中的第一个 +
在执行的时候,第三个++
是否执行,这个是不确定的,因为依靠操作符的优先级和结合性是无法决定第一个 + 和第三个前置 ++ 的先后顺序。因此在不同的编译器上运算结果不同。
总结
本章主要介绍了两种隐式类型转换以及操作符属性,重点在于理解并掌握两种转换方式,以及灵活运用操作符属性进行表达式求值。
特别注意:
我们在写表达式时,要保证在求值时计算路径是唯一的。如果我们写出的表达式不能通过操作符的属性确定唯一的计算路径,那这个表达式就是存在问题的。所以请不要折磨编译器,杜绝写出以上华而不实的代码!!!