【C语言航路外传】隐式转换与优先级的那点事(你程序总是出bug的一个重要原因)

简介: 【C语言航路外传】隐式转换与优先级的那点事(你程序总是出bug的一个重要原因)

一、表达式求值

在我们前面介绍了那么多的操作符,我们肯定肯定是需要使用他们的,在使用他们的时候,就会出现各种各样很奇怪的状况。这是因为我们还没有了解一些优先级相关的知识和一些隐式类型转换的问题。所以,我们这部分就来仔细描述一下有关类型转换的那些事。

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

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

二、隐式类型转换

1.基本概念

所谓隐式类型转化,就是偷偷的发生转换,你没有察觉到的一些转换。这种转换,如果不了解,往往会出现一些难以发现的错误。

c的整型算术运算总是以缺省整型类型的精度来进行的(缺省的意思是默认)

为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种转换称为整型提升

我们直接举一个例子

#include<stdio.h>
int main()
{
  char a = 3;
  char b = 127;
  char c = a + b;
  printf("%d", c);
  return 0;
}

由于我们总是以缺省整型类型的精度来进行计算的,所以我们这个char类型的数据在运算的时候会先转化为int类型。然后在进行计算,这就是整型提升

2.整型提升的意义

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

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

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

3.详解截断与整型提升的过程

整形提升是按照变量的数据类型的符号位来提升

无符号的整型提升直接补0

我们还是看这段代码

#include<stdio.h>
int main()
{
  char a = 3;
  char b = 127;
  char c = a + b;
  printf("%d", c);
  return 0;
}

这段代码他一开始是将3赋给a,这里要注意,这个3是一个整数,而整数是四个字节,也就是32个比特位,我们将鼠标放在编译器中的这个3上

我们从这里也能看出来,3是一个整型的。所以我们得先将3的二进制序列写出来

他的原码、反码、补码均为:00000000 00000000 00000000 00000011

而我们这个3是要存放到a里面去的,a是一个char类型,他只有一个字节,所以会发生截断现象,从右往左数,拿走他需要的比特位,其余的统统砍掉不要了

所以3放到a里面的二进制序列就变为了00000011

同理b也一样

127的二进制序列为00000000 00000000 00000000 01111111

127放到b里面去,自然要发生截断现象

所以此时a与b要进行相加

而此时由于整型提升补的是数据的符号位,我们这个char类型其实本质上应该是signed char,是有符号类型的,所以最高位就是符号位 ,所以补符号位,补成int类型的字节

a提升后为00000000 00000000 00000000 00000011  补的是符号位,符号位为0

b提升后为00000000 00000000 00000000 01111111  补的是符号位,符号位为0

a与b提升后相加为00000000 00000000 00000000 10000010

而我们相加后的结果又要放到c里面去。这里就又发生截断了

此时c为10000010

此时,接下来就要进行打印了。%d是以十进制进行打印,c为char类型,因此又要发生整型提升了,c为11111111 11111111 11111111 10000010,按符号位进行提升

要注意的是,我们的数据在内存中都是补码的形式,进行计算的,我们上述的操作都是补码,所以此时提升后的也是一个补码。既然是补码就要转换为原码了

原码为10000000 00000000 00000000 01111110

转换成十进制数就是-126

所以最终打印出来的结果就是-126

4.char类型范围有关的一些事情

char------有符号类型的char取值范围是:-128~127

              无符号的char取值范围是:  0~255

那么这些范围是如何得到的呢?实际上是计算出来的,而不是规定出来的

因为一个char类型是一个字节,也就是八个比特位,因此他的二进制序列的可能性就下面图中所示的这些,而这些总共有256种可能性

假设我们现在讨论的是有符号的数,在下面图中所示的二进制中,第一位代表的是符号位,0为正数,1为负数

我们把这些东西存到内存中就叫做补码。然后将他们分别计算出来就是这些数

所以有符号类型的char范围就是-128~127

而无符号类型的就很简单了,因为他们没有负数。所以直接就是他们计算成的十进制数,如下图所示,所以他的范围就是0~255

5.有关整形提升的一些案例

我们看这段代码,并思考运行结果

#include<stdio.h>
int main()
{
  char a = 0xb6;
  short b = 0xb600;
  int c = 0xb6000000;
  if (a == 0xb6)
    printf("a");
  if (b == 0xb600)
    printf("b");
  if (c == 0xb6000000)
    printf("c");
  return 0;
}

运行结果为

这是因为a和b他都是小于int类型的,所以都会发生整型提升。而他们的符号位都是1,所以补的都是1,肯定不一样。所以为c。(上面这些数都是16进制数。两个十六进制数代表一个字节)

还有这一段代码

#include<stdio.h>
int main()
{
  char c = 1;
  printf("%u\n", sizeof(c));
  printf("%u\n", sizeof(+c));
  printf("%u\n", sizeof(-c));
  return 0;
}

这个运行结果为

这是因为c进行了运算,所以被提升了。因为+,-也是一个操作符。

三、算术转换

小于int会发生整型提升,那么大于int呢?其实会发生算术转换

如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类型,否则操作就无法进行。下面的层次体系称为寻常算术转换。从上到下层级依次降低

long double

double

float

unsigned long int

long int

unsigned int

int

如果某个操作数的类型在上面这个列表中排名较低,那么首先要转换为另外一个操作数的类型后执行运算。

比如

a=3;

b=3.14;

c=a+b;

那么a首先会转换成b的类型进行计算

但是进行算术转换要合理,否则会出现问题

float a=3.14

int b=a;隐式转换,会出现精度缺失,但是编译器只会报警告,不会报错

四、操作符的属性

复杂表达式的求值有三个影响的因素。

1. 操作符的优先级

2. 操作符的结合性

3. 是否控制求值顺序。

相邻操作符才考虑优先级,两个相邻的操作符优先执行哪一个?取决于他们的优先级。如果两者的优先级相同,才取决于他们的结合性

1.优先级表格

操作符     描述 用法用例 结果类型 结合性 是否控制求值顺序
() 聚组 (表达式) 与表达式同 N/A
() 函数调用 rexp(rexp,....,rexp) rexp L-R
 [ ] 下标引用 rexp[rexp] lexp L-R
. 访问结构成员 lexp.member_name lexp L-R

->

访问结构指针成员 rexp->member_name lexp L-R
++ 后缀自增 lexp++ rexp L-R
-- 后缀自减 lexp-- rexp L-R

逻辑反 !rexp rexp R-L

~ 按位取反 ~rexp rexp R-L
+ 单目,表示正值 +rexp rexp R-L

- 单目,表示负值 -rexp rexp R-L
++ 前缀自增 ++lexp rexp R-L
-- 前缀自减 --lexp rexp R-L
* 间接访问 *rexp lexp R-L
& 取地址 &lexp rexp R-L
sizeof 取其长度,以字节表示

sizeof rexp

sizeof(类型)

rexp R-L
(类型) 类型转换 (类型)rexp rexp R-L
* 乘法 rexp*rexp rexp L-R
/ 除法 rexp/rexp rexp L-R
% 整数取余 rexp%rexp rexp L-R
+ 加法 rexp+rexp rexp L-R
- 减法 rexp-rexp rexp L-R
<< 左移位 rexp<<rexp rexp L-R
>> 右移位 rexp>>rexp rexp L-R
> 大于 rexp>rexp rexp L-R
>= 大于等于 rexp>=rexp rexp L-R
< 小于 rexp<rexp rexp L-R
<= 小于等于 rexp<=rexp rexp L-R
== 等于 rexp==rexp rexp L-R
!= 不等于 rexp!=rexp rexp L-R
& 位与 rexp&rexp rexp L-R
^ 位异或 rexp^rexp rexp L-R
| 位或 rexp|rexp rexp L-R
&& 逻辑与 rexp&&rexp rexp L-R
|| 逻辑或 rexp||rexp rexp L-R
?: 条件操作符 rexp?rexp:rexp rexp N/A
= 赋值 lexp=rexp rexp R-L
+= 以...加 lexp+=rexp rexp R-L
-= 以...减 lexp-=rexp rexp R-L
*= 以...乘 lexp*=rexp rexp R-L

/=

以...除 lexp/=rexp rexp R-L
%= 以...取模 lexp%=rexp rexp R-L
<<= 以...左移 lexp<<rexp rexp R-L

>>= 以...右移 lexp>>rexp rexp R-L
&= 以...与 lexp&=rexp rexp R-L
^= 以...异或 lexp^=rexp rexp R-L
|= 以...或 lexp|=rexp rexp R-L

逗号 rexp,rexp rexp L-R

2.运算规则

首先确定优先级,相邻操作符按照优先级高低计算

优先级相同的情况下,结合性才起作用

我们看这样一段代码

#include<stdio.h>
int main()
{
  int a = 1;
  int b = 2;
  int c = 4;
  int d = a * 4 + b / 3 + c;
  return 0;
}

这个表达式先算a*4,然后计算b/3,然后计算第一个加法,然后计算第二个加法。

3.一些问题表达式

(1)a*b+c*d+e*f

a*b+c*d+e*f

这个表达式种,我们按照从左到右给他分别记作 1 2 3 4 5

那么他的计算顺序可以是1 3 2 5 4

也可以是1 4 2 5 3

这就出现两种运算方式。虽然结果是一样的,但是出现了两种计算方式,这就是很危险的行为了。比如将a看作一个表达式,如果他出现了副作用,那么势必会影响结果。

(1)c+ --c

这个也存在问题,我们知道先算--,然后计算加法

但是我们计算的是2 +2 呢还是3+2

也就是说虽然运算顺序知道了,但出现了副作用的表达式。第一个c会不会改变是取决于编译器的。看他是什么时候拿出他的值的。因此这个也存在潜在的危险

(3)i=i-- - --i*(i=-3)*i++ + ++i

这个代码,将他放在不同的编译器上,甚至每个编译器的结果各不相同。这是极其危险的代码

(4)调用函数时

#include<stdio.h>
int fun()
{
  static int count = 1;
  return ++count;
}
int main()
{
  int anwer;
  anwer = fun() - fun() * fun();
  printf("%d", anwer);//输出多少?
  return 0;
}

这段代码也同样使得编译器凌乱了,不知道该如何做。不同的编译器有不同的结果

(5)总结

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


总结

本节主要讲解了表达式求值,隐式转换,整型提升,算术转换,操作符的优先级,结合性,以及是否控制求值顺序的一些知识点

如果对你有帮助,不要忘记点赞+收藏哦!!!

相关文章
|
3月前
|
存储 自然语言处理 编译器
【C语言】编译与链接:深入理解程序构建过程
【C语言】编译与链接:深入理解程序构建过程
|
5月前
|
存储 算法 C语言
"揭秘C语言中的王者之树——红黑树:一场数据结构与算法的华丽舞蹈,让你的程序效率飙升,直击性能巅峰!"
【8月更文挑战第20天】红黑树是自平衡二叉查找树,通过旋转和重着色保持平衡,确保高效执行插入、删除和查找操作,时间复杂度为O(log n)。本文介绍红黑树的基本属性、存储结构及其C语言实现。红黑树遵循五项基本规则以保持平衡状态。在C语言中,节点包含数据、颜色、父节点和子节点指针。文章提供了一个示例代码框架,用于创建节点、插入节点并执行必要的修复操作以维护红黑树的特性。
116 1
|
5月前
|
NoSQL 编译器 程序员
【C语言】揭秘GCC:从平凡到卓越的编译艺术,一场代码与效率的激情碰撞,探索那些不为人知的秘密武器,让你的程序瞬间提速百倍!
【8月更文挑战第20天】GCC,GNU Compiler Collection,是GNU项目中的开源编译器集合,支持C、C++等多种语言。作为C语言程序员的重要工具,GCC具备跨平台性、高度可配置性及丰富的优化选项等特点。通过简单示例,如编译“Hello, GCC!”程序 (`gcc -o hello hello.c`),展示了GCC的基础用法及不同优化级别(`-O0`, `-O1`, `-O3`)对性能的影响。GCC还支持生成调试信息(`-g`),便于使用GDB等工具进行调试。尽管有如Microsoft Visual C++、Clang等竞品,GCC仍因其灵活性和强大的功能被广泛采用。
153 1
|
29天前
|
C语言
【C语言】符号优先级详解 -《谁与争锋 ! 》
理解C语言中的运算符优先级和结合性是编写正确代码的关键。本文详细介绍了C语言中的各种运算符、它们的优先级和结合性,并通过示例展示了如何正确使用这些运算符。掌握这些知识,将有助于编写出逻辑严谨、结构清晰的C语言程序。
82 8
|
5月前
|
编译器 C语言 计算机视觉
C语言实现的图像处理程序
C语言实现的图像处理程序
224 0
|
2月前
|
存储 缓存 算法
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式,强调了合理选择数据结构的重要性,并通过案例分析展示了其在实际项目中的应用,旨在帮助读者提升编程能力。
60 5
|
2月前
|
C语言
C语言编程中,错误处理至关重要,能提升程序的健壮性和可靠性
C语言编程中,错误处理至关重要,能提升程序的健壮性和可靠性。本文探讨了C语言中的错误类型(如语法错误、运行时错误)、基本处理方法(如返回值、全局变量、自定义异常处理)、常见策略(如检查返回值、设置标志位、记录错误信息)及错误处理函数(如perror、strerror)。强调了不忽略错误、保持处理一致性及避免过度处理的重要性,并通过文件操作和网络编程实例展示了错误处理的应用。
72 4
|
2月前
|
并行计算 算法 测试技术
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面,旨在通过综合策略提升程序性能,满足实际需求。
63 1
|
2月前
|
网络协议 物联网 数据处理
C语言在网络通信程序实现中的应用,介绍了网络通信的基本概念、C语言的特点及其在网络通信中的优势
本文探讨了C语言在网络通信程序实现中的应用,介绍了网络通信的基本概念、C语言的特点及其在网络通信中的优势。文章详细讲解了使用C语言实现网络通信程序的基本步骤,包括TCP和UDP通信程序的实现,并讨论了关键技术、优化方法及未来发展趋势,旨在帮助读者掌握C语言在网络通信中的应用技巧。
47 2
|
2月前
|
程序员 C语言
C语言中的指针既强大又具挑战性,它像一把钥匙,开启程序世界的隐秘之门
C语言中的指针既强大又具挑战性,它像一把钥匙,开启程序世界的隐秘之门。本文深入探讨了指针的基本概念、声明方式、动态内存分配、函数参数传递、指针运算及与数组和函数的关系,强调了正确使用指针的重要性,并鼓励读者通过实践掌握这一关键技能。
44 1