C语言:表达式求值(整型提升、算术转换 ...)

表达式求值

表达式求值的顺序,一部分是由操作符的优先级和结合性决定的。

同样,有些表达式的操作数在求值的过程中可能需要转换成其它类型。

表达式求值,先看有没有隐式类型转换(整型提升/算数转换),然后再看操作符的优先级和结合性

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<stdio.h>
int main()
{
	char a = 3;
    //a:00000011 - 补码,符号位为0
	char b = 127;
    //b:01111111 - 补码,符号位为0
    
    char 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<stdio.h>
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<stdio.h>
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)】

image-20210521205900237

unsigned char 整数取值范围:0~255【 0000 0000(0) ~ 1111 1111(255)】

image-20210522144429458

巧记口诀:

signed/unsigned char 超出范围的数据如果是正数,则减去256;超出范围的数据如果是负数,则加上256。

例:

char a = 200;
printf("%d", a);
输出:200-256 = -56

char a = -129;
printf("%d", a);
输出:-129+256 = 127

char 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 && rexprexpL-R
||逻辑或rexp || rexprexpL-R
? :条件操作符rexp1 ? rexp2 : rexp3rexpN/A
,逗号rexp , rexp , ……rexpL-R

逻辑与,当左边表达式为假,右边是真还是假不重要了,因为它整体就为假了,所以右边表达式就不会运算了

所以它会控制求值顺序,让某一个部分会运算,而让某一部分不会去运算了。

条件操作符,表达式1成立,表达式2计算,而表达式3就不会计算了。

逗号操作符,整个表达式从左到右都会依次计算,但真正起到作用的,是最后一个表达式。


2、一些问题表达式

学习完上面的表达式求值顺序后,是不是只要写出一个表达式,我们就能求出它的值呢?来看下面的例子

代码1:

a*b + c*d + e*f

表达式在计算的时候,由于 *+ 的优先级高,只能保证 * 的计算比 + 早,但优先级并不能决定第三个 * 和第一个 + 的计算先后顺序,优先级和结合性只有在相邻操作符间才有意义

它的求值顺序是哪一种呢,这个?

image-20210520224833947

还是这个?

image-20210520225013635

好像两种都没有问题,都有理由说得通,但没有办法确定唯一的计算路径

所以,在写代码的过程中,一定要避免写出这种有歧义的代码,可以把它拆分写成三个语句,然后再相加,这样计算顺序就是可控的

代码2:

c + --c;

操作符的优先级只能决定 -- 的运算在 + 的前面,但我们不能确定 + 操作符的左操作数的值是在 --c 之前准备好的,还是 --c 之后准备好的。这也是一个有歧义的表达式。

image-20210521111203770

注意:

以上两个有歧义的表达式,在不同的编译器中会产生不同的结果,为了避免这种情况发生,我们在写代码的时候,我们不要把多个步骤写在一个表达式中,按照你自己的想要实现的计算顺序,一步一步拆开来写,算出每一步的结果,再参与到整个表达式中计算。

代码3:

int fun()
{
    static int count = 1;
    return ++count;
}
int main()
{
    int answer;
    answer = fun() - fun() * fun();
    //大多数编译器上输出 -10
    printf( "%d\n", answer);
    return 0;
}

通过操作符的优先级知道,先算乘法 *,再算减法 - ,但函数的调用顺序无法通过操作符的优先级确定。

所以这个代码依旧存在一些问题。

函数的调用顺序到底是哪一种呢?

是这种?

image-20210521113903492

还是这种?

image-20210521114211542

代码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

image-20210521120603639

开启调试,打开调试 - 窗口 - 反汇编,查看汇编代码,执行过程一目了然

image-20210521122636367

Linux环境 gcc 编译器,先依次计算第一个和第二个 ++i 算出 i 的值为 3,然后计算第一个 + ,得出 3 + 3 = 6,然后再计算第三个 ++i 算出 i 的值为 4,然后计算第二个 + ,得出 6 + 4 = 10


总结:

我们写出的表达式如果不能通过操作符的属性确定唯一的计算路径,那这个表达式就是存在问题的。

  • 18
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 17
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 17
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值