- 表达式求值
- 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),都必须先转换为int
或unsigned 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 - 补码,符号位为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
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 则为正
【首位小于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)】
unsigned char 整数取值范围:0~255【 0000 0000(0) ~ 1111 1111(255)】
巧记口诀:
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、 *** 作符的优先级推荐文章: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
总结:
我们写出的表达式如果不能通过 *** 作符的属性确定唯一的计算路径,那这个表达式就是存在问题的。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)