文章目录
-
- 表达式求值
-
- 1)整型提升(隐式类型转换)
-
- 1、如何进行整型提升
- 2、整型提升的例子
- 3、一些补充:char取值范围
- 2)算术转换(隐式类型转换)
- 3)操作符属性
-
- 1、操作符的优先级
- 2、一些问题表达式
表达式求值 表达式求值的顺序,一部分是由操作符的优先级和结合性决定的。
同样,有些表达式的操作数在求值的过程中可能需要转换成其它类型。
表达式求值,先看有没有隐式类型转换(整型提升/算数转换),然后再看操作符的优先级和结合性
1)整型提升(隐式类型转换)
先来看一段程序:
int main()
{
char a = 3;
char b = 127;
char c = a + b;
printf("%d\n", c);
// -126
return 0;
}
相信很多初学者看到此段代码,都会以为程序会输出 130 ,但其实运行发现,正确结果是 -126
为什么呢?这就涉及到下面要讲的隐式类型转换中的整型提升了
C语言的整型算术运算总是至少以缺省整型类型的精度来进行的。为了获得这个精度,表达式中的字符 char 和短整型 short 操作数在使用之前被转换成普通整型 int ,这种类型转换称为整型提升。
int 类型是最适应计算机系统架构的整数类型,它具有和 CPU 寄存器相对应的空间大小和位格式。
- 整型提升的意义:
表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度一般就是 int 的字节长度,同时也是CPU的通用寄存器的长度。在运算中,char 类型和 short 类型都没有达到一个 int 类型的大小,但 CPU 又是以整型 int 的方式来计算的,如果我们能够把长度小于 int 的提升成 int 来计算,这样计算的精度就提高了。
因此,即使两个 char 类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长度。
通用CPU(general-purpose CPU)是难以直接实现两个 8 比特字节直接相加运算(虽然机器指令中可能有这种字节相加指令)。所以,表达式中各种长度可能小于int
长度的整型值(char和short),都必须先转换为int
或unsigned int
,然后才能送入CPU去执行运算。
- 截断:
在C语言中进行变量赋值的时候,赋值了超出范围的数据,即将整数存入比它占字节小的变量类型中时,就会发生截断,保留相应的整数二进制序列的低位,其余部分抛弃。实例:
b 和 c 的值被提升为普通整型,然后再执行加法运算。
加法运算完成之后,结果将被截断,然后再存储于 a 中。
char a, b, c;
a = b + c;
1、如何进行整型提升
- 负数的整型提升
char a = -128;
负数 -128 在 char 类型的取值范围内,其在内存中的补码形式:
10
10000000 - a(补码)
变量 a 是一个有符号数,最高位为 1 表示负数,所以整型提升的时候,高位补充符号位,即为 1
整型提升之后的结果为:
11111111 11111111 11111111 10000000 - a(补码)
- 正数的整型提升
char a = 1;
正数 1 在 char 类型的取值范围内,其在内存中补码形式:
0000 0001 - a
因为变量 a 是一个有符号数,最高位为 0 表示正数,所以整型提升的时候,高位补充符号位,即为 0
整型提升之后的结果为:
00000000 00000000 00000000 00000001 - a(补码)
- 无符号数整型提升
unsigned char a = 300;
// %u - unsigned int - 按无符号整型数输入或输出数据
//注意:无符号整型数用 %u 打印,否则不会得到正确结果
// %d 表示有符号十进制数的打印
printf("%u", a);
//输出 44
正数 300 超过了 char 类型的取值范围,其在内存中的补码形式:
00000000 00000000 00000001 00101100 - 正数300
变量 a 是无符号类型,表示一个正数
而 char 类型占用一个字节,所以 300 存入变量 a 截断保留低8位的二进制数,其余部分抛弃,得到变量 a 的二进制序列(补码)如下:
0010 1100 - a
无符号数整型提升时高位补 0 ,结果为:
00000000 00000000 00000000 00101100 - a(补码)
将补码转换为原码就是十进制的 44
2、整型提升的例子 实例1:
#include
int main()
{
char a = 3;
//a:00000011 - 补码,符号位为0
char b = 127;
//b:01111111 - 补码,符号位为0char c = a + b;
//(1)a 和 b 要参与运算,但都是 char 类型,没有达到一个 int 类型大小
//所以先要进行整型提升:
//a:00000000 00000000 00000000 00000011 - 补码
//b:00000000 00000000 00000000 01111111 - 补码//(2)进行 a + b 运算:
//a + b:00000000 00000000 00000000 10000010 - 补码
//运算的结果要放到 c 里面,而 c 只能存8个比特位
//所以截断保留低8位:
//c:10000010 - 补码,符号位是1 printf("%d\n", c);
//打印成整型形式,对 c 进行整型提升:
//c:11111111 11111111 11111111 10000010 - 补码
//把补码转换成原码即为 -126
//c:11111111 11111111 11111111 10000001 - 反码
//c:10000000 00000000 00000000 01111110 - 原码:-126 return 0;
}
运行结果:
-126实例2:
#include
int main()
{
char a = 0xb6;
//0000 0000 0000 0000 0000 0000 1011 0110 - 0xb6(182)
//截断:
//1011 0110 - a(补码,符号位为1)(-54)
short b = 0xb600;
//0000 0000 0000 0000 1011 0110 0000 0000 - 0xb600(46592)
//截断:
//1011 0110 0000 0000 - b(补码,符号位为1)(-13824)
int c = 0xb6000000;
if (a == 0xb6)
printf("a");
if (b == 0xb600)
printf("b");
if (c == 0xb6000000)
printf("c");
return 0;
}
运行结果为:
c运行结果分析,两种理解的角度:
- 因为变量 a 和 b 没有达到一个 int 的大小,所以在参与表达式
a == 0xb6
运算时,被整型提升,生成一个int类型的临时变量与0xb6
比较,所以 if 表达式为假,变量 c 不需要整型提升。
- 变量 a 是 char 类型, 0xb6 不在该类型取值范围内,存不下,会被截断,所以变量 a 的大小就不是 0xb6 了,if 表达式为假;变量 b 也一样。
- 补充知识点:如何判断十六进制的正负
把第一个十六进制位转换成 4 个二进制,高位为 1 则为负,为 0 则为正实例3:
【首位小于7(即 0~7 )为正,大于或等于8(即 8~F ) 为负】
如 0xb6 :第一个十六进制位 b --> 二进制位 1011(所以 0xb6 为负)
int main()
{
char c = 1;
//sizeof返回无符号整型数,所以用 %u 打印
printf("%u\n", sizeof(c));
//输出1
printf("%u\n", sizeof(+c));
//输出4
printf("%u\n", sizeof(-c));
//输出4
printf("%u\n", sizeof(!c));
//输出4(gcc编译器下)
return 0;
}
第4个输出语句,VS中可能会输出 1 ,我们以 gcc 编译器为准,更加符合C语言的标准,VS编译器有时候在实现的时候没有尊重C语言的标准
运行结果分析:
没有达到一个 int 类型的大小的变量只要参与表达式运算(但实际本代码比如:sizeof()
中的表达式并没有参与运算,任何一个变量都具有值属性和类型属性,虽然不会真的运算,但是总要得出一个结果吧,所以会推导出来如果参与运算了,它的内存大小是多少),就会发生整型提升,所以代码中+c
、-c
、!c
表达式中的变量 c 都会发生整型提升,sizeof()
的结果是 4 个字节
sizeof()
中的表达式是不参与运算的,只会假设运算,s 的值是不会改变的。#include
int main()
{
short s = 5;
int a = 4;
printf("%d\n", sizeof(s = a + 6));
printf("%d\n", s);
}
运行结果:
23、一些补充:char取值范围 要注意C语言中赋值时超出范围的数据的计算方法
5
对 char 类型变量赋值最容易超出范围,记得要截断保留低8位哦
signed char 整数取值范围: -128~127【 1000 0000(-128) ~ 0111 1111(127)】
unsigned char 整数取值范围:0~255【 0000 0000(0) ~ 1111 1111(255)】
文章图片
文章图片
巧记口诀:例:
signed/unsigned char 超出范围的数据如果是正数,则减去256;超出范围的数据如果是负数,则加上256。
char a = 200;
printf("%d", a);
输出:200-256 = -56char a = -129;
printf("%d", a);
输出:-129+256 = 127char a = -130;
printf("%d", a);
输出:-130+256 = 126
所以:
无论你往 signed char 类型变量里放多大的数字,因为 char 只能存8个比特位,所以变量中截断保留的数值范围始终在-128~127之间
无论你往 unsigned char 类型变量里放多大的数字,因为 char 只能存8个比特位,所以变量中截断保留的数值范围始终在0~255之间
2)算术转换(隐式类型转换)
某个操作符的各个操作数属于不同类型,那么除非其中一个操作数转换成另一个操作数的类型,否则操作无法进行。下面的层次体系称为寻常算数转换。
long double
double
float
unsigned long int
long int
unsigned int
int
如果某个操作数的类型在上面列表中排名较低,首先要转换成另外一个排名高的操作数的类型后,再执行运算。
从列表下到上转换,即字节短的操作数向字节长的转换,字节长度相同,精度低的向精度更高的转换。
int a = 5;
float b = 4.5;
//此时 a 要转换成 float 类型才能跟 b 进行计算
a + b;
3)操作符属性
观察下面这个表达式
a + b * 3
,没有整型提升,也没有算术转换,那么它的值就会受到操作符属性的影响int a = 3;
int b = 5;
int c = a + b * 3;
复杂表达式的求值有三个影响因素:
- 操作符的优先级
- 操作符的结合性
- 是否控制求值顺序
1、操作符的优先级 推荐文章:C语言运算符优先级和结合性一览表
这里重点讲解一下需要注意的一些操作符
rexp:表达式 L-R:从左到右
- 会控制求值顺序的操作符
操作符 | 描述 | 用法示例 | 结果类型 | 结合性 | 是否控制求值顺序 |
---|---|---|---|---|---|
&& | 逻辑与 | rexp && rexp | rexp | L-R | 是 |
|| | 逻辑或 | rexp || rexp | rexp | L-R | 是 |
? : | 条件操作符 | rexp1 ? rexp2 : rexp3 | rexp | N/A | 是 |
, | 逗号 | rexp , rexp , …… | rexp | L-R | 是 |
所以它会控制求值顺序,让某一个部分会运算,而让某一部分不会去运算了。
条件操作符,表达式1成立,表达式2计算,而表达式3就不会计算了。
逗号操作符,整个表达式从左到右都会依次计算,但真正起到作用的,是最后一个表达式。
2、一些问题表达式 学习完上面的表达式求值顺序后,是不是只要写出一个表达式,我们就能求出它的值呢?来看下面的例子
代码1:
a*b + c*d + e*f
表达式在计算的时候,由于它的求值顺序是哪一种呢,这个?*
比+
的优先级高,只能保证*
的计算比+
早,但优先级并不能决定第三个*
和第一个+
的计算先后顺序,优先级和结合性只有在相邻操作符间才有意义。
还是这个?
文章图片
好像两种都没有问题,都有理由说得通,但没有办法确定唯一的计算路径
文章图片
所以,在写代码的过程中,一定要避免写出这种有歧义的代码,可以把它拆分写成三个语句,然后再相加,这样计算顺序就是可控的
代码2:
c + --c;
操作符的优先级只能决定注意:--
的运算在+
的前面,但我们不能确定+
操作符的左操作数的值是在--c
之前准备好的,还是--c
之后准备好的。这也是一个有歧义的表达式。
文章图片
以上两个有歧义的表达式,在不同的编译器中会产生不同的结果,为了避免这种情况发生,我们在写代码的时候,我们不要把多个步骤写在一个表达式中,按照你自己的想要实现的计算顺序,一步一步拆开来写,算出每一步的结果,再参与到整个表达式中计算。代码3:
int fun()
{
static int count = 1;
return ++count;
}
int main()
{
int answer;
answer = fun() - fun() * fun();
//大多数编译器上输出 -10
printf( "%d\n", answer);
return 0;
}
通过操作符的优先级知道,先算乘法函数的调用顺序到底是哪一种呢?*
,再算减法-
,但函数的调用顺序无法通过操作符的优先级确定。
所以这个代码依旧存在一些问题。
是这种?代码4:
文章图片
还是这种?
文章图片
int main()
{
int i = 1;
int ret = (++i) + (++i) + (++i);
printf("%d\n", ret);
printf("%d\n", i);
return 0;
}
通过操作符优先级知道,VS编译环境中,先依次计算三个++
的运算在+
的前面,但无法确定第三个++
和第一个+
的计算先后顺序
++i
算出 i 的值为 4 ,然后再算加法 +
,得出 12开启调试,打开调试 - 窗口 - 反汇编,查看汇编代码,执行过程一目了然
文章图片
Linux环境 gcc 编译器,先依次计算第一个和第二个
文章图片
++i
算出 i 的值为 3,然后计算第一个 +
,得出 3 + 3 = 6,然后再计算第三个 ++i
算出 i 的值为 4,然后计算第二个 +
,得出 6 + 4 = 10总结:
【C语言学习之路|C语言(表达式求值(整型提升、算术转换 ...))】我们写出的表达式如果不能通过操作符的属性确定唯一的计算路径,那这个表达式就是存在问题的。
推荐阅读
- 大数据|C语言每日题目练习与巩固
- C|【数据结构】 之 表达式求值
- 操作符|操作符中表达式求值(隐式类型转换详解)以及操作符属性
- 数据结构|数据结构 实验三 算术表达式求值 栈的基本操作
- 小杨带你玩转C语言【初阶】|操作符知识你会了,那表达式求值呢()
- c语言学习|c语言深入浅出,玩爆常见字符串,内存操作库函数(爆肝最长时间之作)
- C语言进阶知识|【C语言字符和字符串的库函数的使用注意事项和模拟】
- C入门|指针与字符串,读取字符串,字符串库函数举例 C语言入门
- C语言|C语言课程设计|学生成绩管理系统(含完整代码)