一文带你搞定考试复杂表达式求值问题(和bug郭一起学C系列)

简介: 一文带你搞定考试复杂表达式求值问题(和bug郭一起学C系列)

小试牛刀

//复杂表达式求值案例一
#include<stdio.h>
int mian()
{
  int i=1;
  int c= (++i) + (i++) + (i++);
  printf("C=%d",c);
  return 0;
}
//复杂表达式求值案例二
int fun()
{
  static int i=1;
  i++;
  return i;
}
#include<stdio.h>
int main()
{
  int sum=fun()+fun()+fun();
  printf("sum=%d",sum);
 return 0;
}

image.png


表达式求值

啥是表达式求值呢?

通俗的说就是,像我们加减乘除算数运算一样,通过计算求得运算结果,而C语言不止加减乘除运算,所有的C语言操作符,计算而得出结果,这就是表达式求值!

表达式求值的顺序

我们已经知道,表达式求值就是操作符运算的结果。像加减乘除都有自己的运算顺序,所以操作符都有自己的运算顺序。

而我们知道C语言有很多操作符,每个操作符的优先级,和结合性又不一样!


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

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


隐式类型转换

什么是隐式类型类型转换

我们C语言中的数据类型很多,当不同类型的数据进行运算时,某一类数据,就会进行类型转换而后进行运算。

C语言中的隐式类型转换规则


C语言中的整型算术运算总是至少以缺省整型类型的精度来进行的。

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


有人就该疑惑了,为啥C语言中要以整型的精度进行计算呢,为啥不能是浮点型?

整型提升的意义:


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

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

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


我们可以知道计算机中CPU中运算器的运算标准就是整型(int)进行运算,所以当数据类型不足一个整型,计算机会现将该数据进行整型提升,统一成整型,然后送入CPU执行运算!

我们来看看如何进行整型提升


//实例一
char a,b,c;
a=3;  
//3 00000000 00000000 00000000 00000011
//a=3; 00000011  char 只能存8个二进制位
//整型3截断后放入a中   
b=2;
//2 00000000 00000000 00000000 00000010
//b=2; 00000010  char 只能存8个二进制位,
//整型2截断后放入b中  
c=a+b;
//c=a+b;进行运算需要CPU运算器(ALU)
//a和b不足一个整型,需要进行整型提升!
// char a =3;提升后 
//3 00000000 00000000 00000000 00000011
//char b=2;提升后
//2 00000000 00000000 00000000 00000010
//a+b
//5 00000000 00000000 00000000 00000101
//将5存入char c中,进行截断 后  c 00000101
printf("c=%d",c);//最后进行%d(整型)打印时 
//又将进行一次整型提升 5
//00000000 00000000 00000000 00000101

看到就短短一行代码,计算机却进行了这么多计算。这就是整型提升的步骤,看来表达式求值不易,计算机都这么麻烦,何况我们。

image.png

其实并不难的只要我们掌握了整型提升的规律,这都小菜一碟。

整型提升规则


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

1.有符号数据类型,整型提升补符号位

2.无符号数据类型,整型提升补0


//实例二
char a = -1;
//-1 原码:10000000 00000000 00000000 00000001
//   补码;11111111 11111111 11111111 11111111 
//   截断a=-1  11111111
unsigned char b = 1;
// 1 原码补码相同
// 00000000 00000000 00000000 00000001
//截断 b=1; 00000001
char c = a + b;
// char a 11111111 有符号字符型 符号位提升
// 11111111 11111111 11111111 11111111
//unsigned char b  00000001 无符号字符型 补0提升
//00000000 00000000 00000000 00000001
//a+b 
//00000000 00000000 00000000 00000000
// char c 截断 00000000
printf("%d",c);
//有符号的形式打印,进行整型提升后
//00000000 00000000 00000000 00000000

看到这里,你肯定会想,这提升了个锤子,这不就是,1+(-1)=0嘛还用得了这么麻烦?


我们看看下面代码你就知道了整型提升的意义


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

打印结果

c

是不是有点出乎意料!


让我一步一步给你分析


int main()
{
  char a=0xb6;
  // 0xb6 00000000 00000000 00000000 10110110 
  //截断放入a中  a  10110110
  short b=0xb600;
  //0xb600 00000000 00000000 10110110 00000000
  //截断放入b中 b  10110110 00000000
  int c=0xb6000000;
  //00000000 00000000 00000000 0xb6000000 存入C中
  if(a==0xb6)
      printf("a");
  //a:10110110 符号位是1 提升
  // 11111111 11111111 11111111 10110110
  // 转换成原码:
  //00000000 00000000 00000000 01001010
  //而0xb6
  // 00000000 00000000 00000000 10110110 
  //显然a!=0xb6;
  if(b==0xb600)
  printf("b");
    //b :10110110 00000000 符号位是1提升后
    //11111111 11111111 10110110 00000000
    //原码:
    //00000000 00000000 01001010 00000000
    //而0xb600 
    //00000000 00000000 10110110 00000000
    //b!=0xb600
  if(c==0xb6000000)
  printf("c");
  return 0;

整型提升要点总结:

计算机中是以数据的补码存储计算的,都是在补码的基础上继续整型提升和计算

整型提升补充的二进制位是要看该数据类型是有符号类型,还是无符号类型。

有符号类型补充符号位,也就是该数据的最左边的二进制位

无符号类型同一补充0

当一个数据进行有符号类型打印%d直接补充该数据的符号位,然后,将该数据转换成原码就是打印的结果

无符号打印时%u直接补充0然后该数据的原反补码相同。

算数转换

可能学会了整型提升的小伙伴就有所疑惑了,当一个数据超过整型呢?

当这些数据是long, long long , double 等数据类型计算时该如何呢?


如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类

型,否则操作就无法进行。下面的层次体系称为寻常算术转换。


该如何转换呢?

也是像整型提升那样,都转换成整型吗?

那必须不是

我们来看看算数转换的规律


//从下往上,层次转换
 long double
 double
 float
 unsigned long int
 long int
 unsigned int
 int

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

许多操作数类型为算数类型的双目运算符会引发转化,
并以类似的方式产生结果类型。他的目的是产生一个普通类型,
同时也是运算结果的类型。这个模式称为“寻常算数转换”。
                        ——ANSI C手册

通俗来说:算数转换朝着精度更高,空间大小更大的类型进行转换。


//错误的转换
float f=3.1415;
int a=f; //隐式转换,精度丢失!

操作符的属性

在C语言操作符详解中,我已经介绍过了C语言中的所有操作符,还没看的伙伴可以点击查看操作符详解!

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


操作符的优先级

两个相邻操作符的计算顺序取决于它们的优先级。


操作符的结合性

当两个操作符的优先级相同时,计算顺序就要看它们的结合性,结合性就是决定从左向右,还是从右向左计算。


是否控制求值顺序。

控制求值顺序就是像||和&&一样会发生短路,当一个假时另一个表达式就停止计算,当一个为真时另一个表达式就停止计算!


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


操作符优先级及结合性

image.png

这是<<C和指针>>一书中操作符优先级表!

学习C语言的同学怎么可以没读过这本书,强烈安利大家学习!有需要电子版的伙伴可以私聊bug郭


操作符的优先级从上到下,由高到低!

表达式的求值部分由优先级决定!


问题表达式

//表达式一
a*b + c*d + e*f;

这个代码可能有多种结果

我们第一步只能确定在第一个乘和第一个加,是先计算a*b,而第三个乘不能确定是否比第一个加早,所以有多种计算方式。

//1
a*b
c*d
a*b+c*d
e*f
a*b+c*d+e*f
//2
a*b
c*d
e*f
a*b+c*d
a*b+c*d+e*f
//表达式二
c + c--;

注释:同上,操作符的优先级只能决定自减--的运算在+的运算的前面,但是我们并没有办法得知,+操作符的左操作数的获取在右操作数之前还是之后求值,所以结果是不可预测的,是有歧义的。


//表达式三,非法表达式
int main()
{
    int i = 10;
    i = i-- - --i * ( i = -3 ) * i++ + ++i;
    printf("i = %d\n", i);
    return 0;
}

上面表达式的结果在不同编译器下结果不一样。


C和指针一书中整理了不同编译器下的结果

image.png


//表达式四
int fun()
{
     static int count = 1;
     return ++count;
}
int main()
{
     int answer;
     answer = fun() - fun() * fun();
     printf( "%d\n", answer);//输出多少?
     return 0;
}

这个代码有没有实际的问题?

有问题!

虽然在大多数的编译器上求得结果都是相同的。

但是上述代码answer = fun() - fun() * fun(); 中我们只能通过操作符的优先级得知:先算乘法,再算减法。函数的调用先后顺序无法通过操作符的优先级确定。

这样的表达式求值问题是压根没有意义的,如果学校再出此类问题,你就将<<C和指针>>一书甩在你老师面前!


//表达式五
#include <stdio.h>
int main()
{
    int i = 1;
    int ret = (++i) + (++i) + (++i);
    printf("%d\n", ret);
    printf("%d\n", i);
    return 0;
}
//尝试在linux 环境gcc编译器,VS2013环境下都执行,看结果。

linux环境gcc编译器结果

10

4

vs2013环境下结果

12

4

看看同样的代码产生了不同的结果,这是为什么?

简单看一下汇编代码,就可以分析清楚!

这我就不带大家研究了,如果有兴趣的伙伴可以研究一下!


这段代码中的第一个+ 在执行的时候,第三个++是否执行,这个是不确定的,因为依靠操作符的优先级和结合性是无法决定第一个+ 和第三个前置++ 的先后顺序。


总结:

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

目录
相关文章
|
9月前
|
人工智能 数据可视化 数据挖掘
从传统软件到SaaS:为什么更多企业选择订阅制服务?
本文详细介绍了SaaS的概念、优势及其在现代工作中的重要性。SaaS是一种通过互联网提供云计算服务,用户无需安装和维护本地软件,只需通过网络访问软件即可。SaaS通过自动更新和维护、订阅制收费模式等方式降低成本,提供更便捷的服务。
1532 4
从传统软件到SaaS:为什么更多企业选择订阅制服务?
|
8月前
|
监控 数据可视化
高效销售管理全攻略:如何确保销售目标的精准实现
销售目标设定是销售管理的核心,科学的方法能提高执行力和业绩。本文探讨如何通过目标层级划分、遵循SMART原则、合理分解与资源分配及进度跟踪,确保销售目标的落地执行。借助板栗看板等工具,可进一步优化团队协作和目标达成率,助力企业持续增长。
423 25
|
11月前
|
API 定位技术
查IP[查指定IP归属地]免费API接口教程
该API用于查询指定IPv4地址的归属地信息,支持POST和GET请求。需提供用户ID和KEY,可选填查询IP,默认为请求接口IP。返回信息包括状态码、地理位置及运营商等。示例请求和响应详见文档。
1509 3
|
存储 网络架构
网络速率与下载速率
【8月更文挑战第8天】
2158 1
网络速率与下载速率
|
12月前
|
网络协议 前端开发 JavaScript
WebSocket 教程汇总指南,从入门到熟练
本文将带你从零开始,逐步掌握 WebSocket 的基本概念、实现方法和应用场景,通过一系列详细的教程和实践案例,帮助你从入门到熟练地使用 WebSocket 技术。无论你是初学者还是有一定经验的开发者,本文都能为你提供有价值的信息和指导。
|
JavaScript Java C#
Java中var的使用方法
这篇文章主要介绍了Java中自JDK10起引入的新特性`var`,这是一种局部变量类型推断功能,旨在简化代码书写并提升开发效率。通过示例展示了如何使用`var`定义不同类型的变量,包括基本数据类型及集合类。作者还特别强调了在使用`var`时的一些限制与注意事项,比如无法定义未初始化的变量、不适用于类成员变量以及不可作为方法参数等。最后,文章指出虽然`var`能带来便利,但也可能影响代码的可读性,建议开发者根据实际情况合理使用。
445 2
|
设计模式 开发者
代码复用的重要性及最佳实践
【8月更文挑战第13天】代码复用是软件开发中不可或缺的一部分。通过复用现有的、经过验证的代码,开发者可以加速开发过程、提高软件质量、降低错误率并促进团队协作。为了实现代码复用,开发者需要遵循模块化设计原则、利用现有的库和框架、封装和抽象代码、遵循设计模式以及编写可复用的代码。同时,他们还需要定期维护和更新复用代码以确保其持续有效。
|
机器学习/深度学习 数据采集 存储
机器学习在推荐系统中的应用
【7月更文挑战第31天】随着机器学习技术的不断发展和普及,推荐系统在电子商务、社交媒体、新闻资讯等领域的应用越来越广泛。机器学习算法的应用为推荐系统优化提供了全新的思路和方法,使得推荐系统能够更加智能化和个性化地为用户提供服务。未来,随着数据量的不断增加和算法的不断创新,推荐系统将会变得更加精准和高效,为用户带来更加优质的体验。
|
存储 前端开发 JavaScript
[初学者必看]JavaScript 简单实际案例练习,锻炼代码逻辑思维
【6月更文挑战第2天】这是一个前端小项目合集,包括图片轮播器、动态列表、模态框、表单验证等14个项目,旨在帮助初学者提升编码技能和实战经验。每个项目提供关键提示,如使用HTML、CSS和JavaScript实现不同功能,如事件监听、动画效果和数据处理。通过这些项目,学习者可以锻炼前端基础并增强实际操作能力。
449 2
|
网络架构
【专栏】网络技术:网速和带宽的区别,带宽是网络的最大传输能力,而网速是实际传输速率,受网络拥堵、硬件性能等因素影响
【4月更文挑战第28天】本文探讨了网速和带宽的区别,带宽是网络的最大传输能力,而网速是实际传输速率,受网络拥堵、硬件性能等因素影响。两者关系可比喻为道路车道数与车辆速度。了解这些有助于优化网络体验,如选择合适带宽、升级硬件、使用有线连接、管理带宽占用和连接时机。理解二者差异能帮助我们更好地评估网络服务并提升上网效率。
1857 1