C语言——表达式的求值

表达式求值有以下几种决定因素。

一、操作符优先级和结合性

类别  操作符  结合性 
后缀  () [] -> . ++ - -   从左到右 
一元  + - ! ~ ++ - - (type)* & sizeof  从右到左 
乘除  * / %  从左到右 
加减  + -  从左到右 
移位  << >>  从左到右 
关系  < <= > >=  从左到右 
相等  == !=  从左到右 
位与 AND  从左到右 
位异或 XOR  从左到右 
位或 OR  从左到右 
逻辑与 AND  &&  从左到右 
逻辑或 OR  ||  从左到右 
条件  ?:  从右到左 
赋值  = += -= *= /= %=>>= <<= &= ^= |=  从右到左 
逗号  从左到右 

计算9 + 2 * 3的值,由于*优先级高,所以先计算2 * 3,再计算9 + 6。

计算2 + 3 + 4的值,由于+与+优先级相同,+结合性是从左向右,所以先计算2 + 3,再计算5 + 4。

二、隐式类型转换

1、定义

隐式类型转换,也称作自动类型转换,是编程语言在表达式求值时自动进行的类型转换,目的是解决不同类型之间的操作。而这一过程是自动发生的,不需要程序员显式指定。编译器根据一定的规则自动进行类型的转换以适应操作的需要。

这些转换通常发生在以下几种情况:

  1. 算术运算中:当算术运算中的操作数类型不匹配时,例如一个int类型和一个float类型相加,较低精度的类型(这里是int)会自动转换为较高精度的类型(这里是float),以便进行计算。

  2. 赋值操作中:当将一个表达式的值赋给一个不同类型的变量时,如将float类型赋给int类型的变量,会进行类型转换。

  3. 函数参数中:当函数调用中实参与形参类型不一致时,实参将根据形参的类型自动转换。

  4. 条件表达式中:比如在比较不同类型的值时,较低等级的类型会提升为较高等级的类型再进行比较。

  5. 初始化时:如果初始化的值与变量类型不匹配时,如用一个double类型的值来初始化int类型的变量。

类型提升规则遵循C语言的类型转换规则,大致的等级顺序从低到高如下:

char < short int < int < unsigned int < long int < unsigned long int < long long int < unsigned long long int < float < double < long double

在类型转换过程中,可能会发生精度损失,尤其是从浮点类型向整数类型转换时,或者是在整数类型之间转换时如果数值大小超出目标类型的表示范围。

2、种类

在C语言中,有几种常见的隐式类型转换情况:

  1. 整型提升(Integer Promotion)

    • 小整数类型(例如 char 和 short)在参与表达式计算前,会被提升(转换)为 int 类型或者如果 int 类型不能表示其值的话,则转换为 unsigned int 类型。
  2. 通常算术转换(Usual Arithmetic Conversions)

    • 如果操作数类型不匹配,转换会按照一定的优先级顺序进行,通常是将类型提升为能包含所有可能值的最宽类型。
  3. 指针转换

    • 在某些情况下,例如将 NULL 赋值给任何指针类型时,NULL(通常定义为 (void*)0)会隐式转换为相应的指针类型。
  4. 数组到指针的解退(Array to Pointer Decaying)

    • 当数组名用作值时(除了 sizeof 和 & 运算符外),它会被隐式地转换为指向数组第一个元素的指针。
  5. 函数到指针的解退(Function to Pointer Decaying)

    • 函数名,当作为值使用时,会转换为指向函数的指针。
  6. 算术类型之间的转换

    • 当两个不同的算术类型(整数和浮点数)参与运算时,较低精度或较小范围的类型会转换为较高精度或较大范围的类型。例如,int 与 double 运算时,int 会转换为 double
  7. 有符号与无符号的整型混合运算

    • 当有符号和无符号整型混合运算时,如果无符号类型不小于有符号类型,则有符号类型会转换为无符号类型。如果有符号类型范围更大,则无符号类型会转换为相应的有符号类型。
  8. 结构体或联合体赋值

    • 在结构体或联合体的赋值操作中,一种类型的结构体或联合体可以被隐式转换为另一种兼容类型的结构体或联合体。

这些隐式转换常常发生在赋值、算术运算及比较运算中,目标是减少类型不匹配所可能引起的问题,但也可能导致意料之外的结果,因此了解这些转换规则对于编写可靠的代码非常重要。

3、整型提升

在C语言中,整型提升是指在表达式计算过程中,较小的整数数据类型(比如 charshort int)自动转换为较大的整数类型(通常是 intunsigned int),以保证数据精度不丢失,并符合处理器的操作优化。C语言的整型算数运算总是至少以缺省整型类型的精度进行的。为了获得这个精度,表达式中的字符型和短整型数据通常在计算前转换为普通整型,这种转换称为整型提升

整型提升的规则如下:

  1. 如果表达式中的所有值都比 int 小(比如 char 或 short),那么它们会被提升到 int 类型。
  2. 如果表达式中的值有的是 unsigned 类型而其他值的范围在 int 之内,那么这些值会被提升到 unsigned int 类型。
  3. 对于有符号类型,当其值可以在 int 中完全表示时,提升结果为 int;否则,如果 int 不能容纳其值,那么结果类型就是 unsigned int(适用于C99和之前的标准)。
  4. 对于枚举类型,整型提升的结果通常也是 int 或 unsigned int
  5. 整型常量的类型由它们的后缀和值决定,如果没有后缀并且值可以用 int 表示,其类型就是 int;否则,如果可以用 long int 表示而不溢出,它的类型就是 long int;以此类推,可能为 long long int

整型提升是为了保证运算的有效性和准确性,因为处理器通常对 int 类型的数据处理得更高效。这也是为什么在很多处理器和编译器中,int 的大小通常与处理器的字长相同的原因。

表达式的整型运算要在CPU的相应运算器件中执行,CPU内整型运算器(ALU)的操作数的字节长度一般就是int的字节长度,同时也是CPU的通用寄存器的长度。

因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标长度。

通用CPU(general-purpose CPU)是难以直接实现两个一字节的数据直接相加运算(虽然机器指令中可能有这种数据相加指令)。所以,表达式中各种长度可能小于int长度的整型值,都必须先转换为int或unsigned int,然后才能送入CPU去执行运算。

在实际编码中,通常不需要显式地做这种转换,因为编译器会自动处理,但是理解这个过程对于保证程序的可移植性和避免类型相关的错误是很有帮助的。

(1)负数整型提升

	char c1 = -1;

变量c1的二进制位(补码)中只有8个bit;

11111111

这里是有符号的char,所以整型提升时,高位补充符号位,即为1,结果为:

11111111 11111111 11111111 11111111

(2)正数整型提升

	char c1 = 1;

变量c1的二进制位(补码)中只有8个bit;

00000000

这里是有符号的char,所以整型提升时,高位补充符号位,即为0,结果为:

00000000 00000000 00000000 00000000

(3)无符整型提升

高位补零

(4)例子1:

#include 

int main()
{
	//这里的二进制数据没特殊指明的都是补码
	char a = 5;
	//5的二进制补码为00000000 00000000 00000000 00000101
	//因为这里数据5为4字节,而char类型为1字节,所以发生截断,直接把前面的3字节的数据截断,只保留最后一个字节的数据
	//a中的值00000101
	char b = 126;
	//126的二进制补码为00000000 00000000 00000000 01111110
	//因为这里数据126为4字节,而char类型为1字节,所以发生截断,直接把前面的3字节的数据截断,只保留最后一个字节的数据
	//b中的值01111110
	char c = a + b;
	//在表达式计算时,发生整型提升
	//a提升为00000000 00000000 00000000 00000101
	//b提升为00000000 00000000 00000000 01111110
	//相加之后结果是00000000 00000000 00000000 10000011
	//因为这里结果为4字节,而char类型为1字节,所以发生截断,直接把前面的3字节的数据截断,只保留最后一个字节的数据
	//所以c中的值是10000011
	printf("%d\n", c);
	//要打印c的值,也要整型提升c中原来是10000011
	//因为为负数,整数提升时直接补符号位1
	//整数提升之后为11111111 11111111 11111111 10000011
	//这里是补码,转换为原码为10000000 00000000 00000000 01111101
	//转换为十进制为-125
	return 0;
}

(5)例子2:

#include 

int main()
{
	//以下二进制数字没有特殊标注的都是补码形式
	char a = 0xb6;
	//0xb6为00000000 00000000 00000000 10110110
	//char类型为1字节,截断后为
	//a中的值为10110110
	short b = 0xb600;
	//0xb600为00000000 00000000 10110110 00000000
	//short为2字节,截断后为
	//b中的值为10110110 00000000
	int c = 0xb6000000;
	//int为4字节,这里不会发生截断
	//c中的值为10110110 00000000 00000000 00000000
	if (a == 0xb6)//这里a发生整型提升,a中的值为10110110,检测a的符号位为1,直接补符号位1
	{			  //变成11111111 11111111 11111111 10110110
				  //转为原码为10000000 00000000 00000000 01001010
				  //转为十进制为-74
				  //这里的0xb6转成十进制为182
				  //所以这里条件不满足,不会打印字符 'a' 
		printf("a");
	}
	if (b == 0xb600)//这里b发生整型提升,b中的值为10110110 00000000,检测b的符号位为1,直接补符号位1
	{			    //变成11111111 11111111 10110110 00000000
				    //转为原码为10000000 00000000 01001010 00000000
				    //转为十进制为-18944
				    //这里的0xb600转成十进制为46592
				    //所以这里条件不满足,不会打印字符 'b' 
		printf("b");
	}
	if (c == 0xb6000000)//这里的c是int类型,所以不用整型提升,c中的值为10110110 00000000 00000000 00000000
	{					//也就是0xb6000000
						//条件满足,打印字符'c'
		printf("c");
	}
	return 0;
}

这里发现常量值被截断:

运行结果为:

(6)例子3:

#include 

int main()
{
	char c = 1;
	printf("%zu\n", sizeof(c));
	//这里没有整型提升,直接计算c的大小,就是char类型的大小
	printf("%zu\n", sizeof(+c));
	//这里发生整型提升,提升为int类型,计算的是int类型的大小
	printf("%zu\n", sizeof(-c));
	//这里发生整型提升,提升为int类型,计算的是int类型的大小
	return 0;
}

运行结果:

C语言——表达式的求值_第1张图片

三、是否控制求值顺序

控制求值顺序的操作符有

1、逻辑与&&

逻辑与"&&": 逻辑与操作符会用于控制计算顺序,因为它会使用“短路”原则。即如果第一个操作数为假,那么无论第二个操作数真假,结果都为假,此时就不会去计算第二个操作数,直接返回结果为假。

2、逻辑或||

逻辑或 "||": 逻辑或同样应用了“短路”原则,如果第一个操作数为真,那么无论第二个操作数真假,结果都为真,此时就不会去计算第二个操作数,直接返回结果为真。

3、条件表达式 ? :

条件操作符也称三元操作符,它基于一个条件来确定使用两个表达式中的哪一个。格式为 表达式1 ? 表达式2 : 表达式3。优先计算表达式1,如果结果为真(即非零),则计算表达式2并返回结果;若为假(即零),则计算表达式3并返回结果。例如:int b = (a > 0 ? 1 : -1); 当 a > 0 成立时 b 等于1,否则等于 -1。

4、逗号表达式exp1,exp2, ... ,expn

逗号操作符能够将多个表达式组成一个表达式,从左至右依次计算每个子表达式,然后返回最右边表达式的结果。例如:int a = (5, 7); 这会使得 a 等于 7。

5、括号()

 在 C 语言中,括号中的表达式优先级最高,括号内的表达式总是先被计算。

四、问题表达式

问题表达式,可能引起一些编译错误或bug。

1、执行顺序不确定

对于一些问题表达式是没有固定的执行顺序的,也就是执行顺序不确定。

例如:a * b + c * d + e * f

假设这里的字母是表达式

因为 * 的优先级大于 + ,所以乘法可以并行计算,但是由于结合性是对于相邻的运算符来判断的,这里的两个 + 不是相邻的,所以无法判断两个 + 谁先执行,所以执行顺序不确定,这是一种问题表达式。

2、没有序列点多次修改变量

序列点:

序列点是一个点,在此之前的所有运算符都已经完成计算,并完成了所有的副作用,同时此点之后的所有运算还未开始的位置。这个概念能够帮助确定表达式的求值顺序。

一些典型的序列点包括:

  1. 一个完全计算出的函数值和传递给该函数的参数值后。
  2. 逻辑AND (&&) 和逻辑 OR (||) 运算符中,确定执行操作数之后。例如,在表达式 a && b 中,a 的计算完全在 b 被计算之前完成。
  3. 条件操作符 (?:) 中,根据条件决定执行哪个部分之后。例如,在表达式 a ? b : c中,a 的计算在 b 或 c 之前完成。
  4. 完整的表达式,在分号之前。在表达式 a = b; 中,b 的求值在 a 被赋值之前完成。

在C语言中,某些情况下,如果在没有序列点的表达式中一个对象被修改两次以上,或者在被修改后被读取同时又被修改,这种情况下编译器会产生未定义的行为

int a = 1;
a = a++ + a;

在这个表达式中,a++a =都会修改a的值,但它们之间没有序列点,导致了未定义的行为。

3、函数调用顺序

函数的调用顺序不能确定。

#include 

int Func()
{
	static int a = 1;
	return ++a;
}

int main()
{
	int b = 0;
	b = Func() + Func() * Func();
	printf("%d\n", b);
	return 0;
}

static修饰的局部变量使编译器在程序的生命周期内保持局部变量的存在,而不需要在每次它进入和退出作用域时进行创建和销毁。详见我之前的文章《C语言——部分关键字(保留字)-CSDN博客》。

通过分析我们可知,第一次调用函数函数会返回2,第二次会返回3,第三次会返回4,但是我们不能确定b = Func() + Func() * Func();中哪一个函数是第一个被调用的,这可能会导致一些编译问题或者一些bug。​​​​​​​

在编程中,上述的问题表达式都应该避免。

你可能感兴趣的:(C语言,c语言)