C语言学习之路|C语言(表达式求值(整型提升、算术转换 ...))


文章目录

    • 表达式求值
      • 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 类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长度。
通用CPU(general-purpose CPU)是难以直接实现两个 8 比特字节直接相加运算(虽然机器指令中可能有这种字节相加指令)。所以,表达式中各种长度可能小于 int 长度的整型值(char和short),都必须先转换为 intunsigned int ,然后才能送入CPU去执行运算。
在运算中,char 类型和 short 类型都没有达到一个 int 类型的大小,但 CPU 又是以整型 int 的方式来计算的,如果我们能够把长度小于 int 的提升成 int 来计算,这样计算的精度就提高了。
  • 截断:
在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
运行结果分析,两种理解的角度:
  1. 因为变量 a 和 b 没有达到一个 int 的大小,所以在参与表达式 a == 0xb6 运算时,被整型提升,生成一个int类型的临时变量与 0xb6 比较,所以 if 表达式为假,变量 c 不需要整型提升。
  2. 变量 a 是 char 类型, 0xb6 不在该类型取值范围内,存不下,会被截断,所以变量 a 的大小就不是 0xb6 了,if 表达式为假;变量 b 也一样。
  • 补充知识点:如何判断十六进制的正负
把第一个十六进制位转换成 4 个二进制,高位为 1 则为负,为 0 则为正
【首位小于7(即 0~7 )为正,大于或等于8(即 8~F ) 为负】
如 0xb6 :第一个十六进制位 b --> 二进制位 1011(所以 0xb6 为负)
实例3:
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); }

运行结果:
2
5
3、一些补充:char取值范围 要注意C语言中赋值时超出范围的数据的计算方法
对 char 类型变量赋值最容易超出范围,记得要截断保留低8位哦
signed char 整数取值范围: -128~127【 1000 0000(-128) ~ 0111 1111(127)】
C语言学习之路|C语言(表达式求值(整型提升、算术转换 ...))
文章图片
unsigned char 整数取值范围:0~255【 0000 0000(0) ~ 1111 1111(255)】
C语言学习之路|C语言(表达式求值(整型提升、算术转换 ...))
文章图片
巧记口诀:
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. 操作符的优先级
  2. 操作符的结合性
  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

表达式在计算的时候,由于 *+ 的优先级高,只能保证 * 的计算比 + 早,但优先级并不能决定第三个 * 和第一个 + 的计算先后顺序,优先级和结合性只有在相邻操作符间才有意义。
它的求值顺序是哪一种呢,这个?
C语言学习之路|C语言(表达式求值(整型提升、算术转换 ...))
文章图片

还是这个?
C语言学习之路|C语言(表达式求值(整型提升、算术转换 ...))
文章图片

好像两种都没有问题,都有理由说得通,但没有办法确定唯一的计算路径
所以,在写代码的过程中,一定要避免写出这种有歧义的代码,可以把它拆分写成三个语句,然后再相加,这样计算顺序就是可控的
代码2:
c + --c;

操作符的优先级只能决定 -- 的运算在 + 的前面,但我们不能确定 + 操作符的左操作数的值是在 --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; }

通过操作符的优先级知道,先算乘法 *,再算减法 - ,但函数的调用顺序无法通过操作符的优先级确定。
所以这个代码依旧存在一些问题。
函数的调用顺序到底是哪一种呢?
是这种?
C语言学习之路|C语言(表达式求值(整型提升、算术转换 ...))
文章图片
还是这种?
C语言学习之路|C语言(表达式求值(整型提升、算术转换 ...))
文章图片
代码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
C语言学习之路|C语言(表达式求值(整型提升、算术转换 ...))
文章图片
开启调试,打开调试 - 窗口 - 反汇编,查看汇编代码,执行过程一目了然
C语言学习之路|C语言(表达式求值(整型提升、算术转换 ...))
文章图片
Linux环境 gcc 编译器,先依次计算第一个和第二个 ++i 算出 i 的值为 3,然后计算第一个 + ,得出 3 + 3 = 6,然后再计算第三个 ++i 算出 i 的值为 4,然后计算第二个 + ,得出 6 + 4 = 10
总结:
【C语言学习之路|C语言(表达式求值(整型提升、算术转换 ...))】我们写出的表达式如果不能通过操作符的属性确定唯一的计算路径,那这个表达式就是存在问题的。

    推荐阅读