C语言——表达式的求值

news2025/5/25 17:48:58

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

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

类别 操作符 结合性 
后缀 () [] -> . ++ - -  从左到右 
一元 + - ! ~ ++ - - (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 <stdio.h>

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 <stdio.h>

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 <stdio.h>

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;
}

运行结果:

三、是否控制求值顺序

控制求值顺序的操作符有

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 <stdio.h>

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。​​​​​​​

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1355768.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Spark内核解析-内存管理7(六)

1、Spark内存管理 Spark 作为一个基于内存的分布式计算引擎&#xff0c;其内存管理模块在整个系统中扮演着非常重要的角色。理解 Spark 内存管理的基本原理&#xff0c;有助于更好地开发 Spark 应用程序和进行性能调优。本文中阐述的原理基于 Spark 2.1 版本。 在执行 Spark 的…

聚丙烯腈,到预测期结束时将达到36亿美元的市场规模

聚丙烯腈 (PAN) 是一种合成聚合物&#xff0c;广泛用于各种应用&#xff0c;包括纺织品、碳纤维生产和水处理。近年来&#xff0c;受航空航天、汽车和建筑行业对碳纤维增强复合材料需求不断增长的推动&#xff0c;全球 PAN 市场稳步增长。 全球 PAN 市场预计从 2020 年到 2025 …

Typora+PicGo+Gitee构建云存储图片

创建Gitee仓库 首先&#xff0c;打开工作台 - Gitee.com&#xff0c;自行注册一个账户 注册完后&#xff0c;新建一个仓库&#xff08;记得仓库要开源&#xff09; 然后创建完仓库后&#xff0c;鼠标移动到右上角头像位置&#xff0c;选择设置&#xff0c;并点击&#xff…

prometheus grafana redis安装配置监控

文章目录 前传安装redis-exporterredis_exporter参数配置参考配置prometheus查看promethues redis job节点grafana配置外传 前传 prometheus grafana的安装使用&#xff1a;https://nanxiang.blog.csdn.net/article/details/135384541 本文说下监控nginx&#xff0c;promethe…

关于户口本等户籍材料翻译

户籍材料&#xff0c;作为证明公民户籍与身份的关键文件&#xff0c;在每个实施户籍制度的国家中均不可或缺。它们不仅是登记、变更或注销户籍的依据&#xff0c;更是多种生活场景中不可或缺的证明。举例来说&#xff0c;一个在国外出生的孩子若想在中国落户&#xff0c;就必须…

【进程间通信】

什么是进程通信 进程通信&#xff08; InterProcess Communication&#xff0c;IPC&#xff09;就是指进程之间的信息的传播和交换。 进程是分配系统资源的单位&#xff0c;包括内存地址空间&#xff0c;为了保证安全&#xff0c;一个进程不能直接访问另一个进程的地址空间&a…

AI:106-基于卷积神经网络的遥感图像地物分类

🚀点击这里跳转到本专栏,可查阅专栏顶置最新的指南宝典~ 🎉🎊🎉 你的技术旅程将在这里启航! 从基础到实践,深入学习。无论你是初学者还是经验丰富的老手,对于本专栏案例和项目实践都有参考学习意义。 ✨✨✨ 每一个案例都附带有在本地跑过的关键代码,详细讲解供…

数字孪生与大数据和分析技术的结合

数字孪生与大数据和分析技术的结合可以为系统提供更深入的见解、支持实时决策&#xff0c;并优化模型的性能。以下是数字孪生在大数据和分析技术中的一些应用&#xff0c;希望对大家有所帮助。北京木奇移动技术有限公司&#xff0c;专业的软件外包开发公司&#xff0c;欢迎交流…

富文本BraftEditor引起的bug

1、BraftEditor踩坑1 #基于之前写的一篇BraftEditor的使用# 1. 问题起源&#xff1a; 打开编辑弹窗--> 下面页面所示--> 当进行分类选择时候&#xff0c;就会报错&#xff0c;并且这个报错还不是一直都有&#xff0c;6次选择出现一次报错吧 2. 解决&#xff1a; 2.1 起…

工具-cmd命令行默认以管理员模式运行

不知道你们电脑是不是遇到过&#xff0c;直接打开命令行&#xff0c;如果执行一些npm nodejs的命令经常会报错&#xff1a; 就必须使用管理员模式才可以。很麻烦&#xff0c;下面是如果设置不管在哪里打开都是管理员模式的方式&#xff1a; 首先找到npm在哪里&#xff1a; 直…

Pytorch一些小知识点 十天快速入门

https://www.aiexplorer.blog/tag/Pytorch Pytorch-day01-基础入门 tensor是什么tensor四则运算tensor广播 view 和 reshape 的区别 view 共享内存&#xff0c;#我们希望原始张量和变换后的张量互相不影响。 #为了使创建的张量和原始张量不共享内存&#xff0c;我们需要使用第…

C#中汉字转区位码

目录 一、关于区位码 1.区位码定义 2.算法 二、实例 三、生成效果 四、程序中的知识点 1.byte[] GetBytes(string s) 2.字节数组转short类型 一、关于区位码 1.区位码定义 区位码是一个4位的十进制数&#xff0c;每个区位码都对应着一个唯一的汉字&#xff0c;区位码…

Sharding-JDBC快速使用【笔记】

1 引言 最近在使用Sharding-JDBC实现项目中数据分片、读写分离需求&#xff0c;参考官方文档&#xff08;Sharding官方文档&#xff09;感觉内容庞杂不够有条理&#xff0c;重复内容比较多&#xff1b;现结合项目应用整理笔记如下供大家参考和自己回忆使用&#xff1b; 在…

计算右侧小于当前元素的个数

题目链接 计算右侧小于当前元素的个数 题目描述 注意点 counts[i] 的值是 nums[i] 右侧小于 nums[i] 的元素的数量 解答思路 本题是交易逆序对的总数的扩展&#xff0c;可以先进入交易逆序对的总数了解&#xff0c;本题与交易逆序对的总数的区别在于需要记录每个元素对逆序…

macOS跨进程通信: XPC 创建实例

一&#xff1a;简介 XPC 是 macOS 里苹果官方比较推荐和安全的的进程间通信机制。 集成流程简单&#xff0c;但是比较绕。 主要需要集成 XPC Server 这个模块&#xff0c;这个模块最终会被 apple 的根进程 launchd 管理和以独立进程的方法唤起和关闭&#xff0c; 我们主app 进…

mysql忘记密码的三种解决方案

1、修改密码的三种方式 mysql用户分为root用户&#xff08;超级管理员&#xff0c;拥有所有权限&#xff09;和普通用户&#xff0c;mysql服务器通过权限表来控制用户对数据库的访问,这些权限表存于root用户下的mysql数据库中。 在使用mysql数据库过程中&#xff0c;往往需要…

DevOps(6)

目录 26.如何在Linux下跨不同的虚拟桌面共享程序&#xff1f; 27.无名&#xff08;空&#xff09;目录代表什么&#xff1f; 29.什么是守护进程&#xff1f; 30.如何从一个桌面环境切换到另一个桌面环境&#xff0c;例如从KDE切换到Gnome? 26.如何在Linux下跨不同的虚拟桌面…

excel中找出重复项,并标红

一、查找重复值 二、清除格式 还是通过添加规则的路径&#xff0c;清除格式&#xff0c;直接通过格式刷&#xff0c;刷不掉。

服务器为什么大多用 Linux?

服务器为什么大多用 Linux&#xff1f; 在开始前我有一些资料&#xff0c;是我根据自己从业十年经验&#xff0c;熬夜搞了几个通宵&#xff0c;精心整理了一份「Linux的资料从专业入门到高级教程工具包」&#xff0c;点个关注&#xff0c;全部无偿共享给大家&#xff01;&#…

AntDB内存管理之内存上下文

1. 主题说明 AntDB的内存管理在开发时&#xff0c;使用了内存上下文机制来实现内存管理。本文就从AntDB的内存上下文机制出发&#xff0c;解析内存上下文的实现原理。AntDB的代码中&#xff0c;涉及到内存的处理时&#xff0c;经常会看到下面这样的代码。 图1&#xff1a;切换…