表达式求值——隐式类型转换与操作符属性

简介: 表达式求值——隐式类型转换与操作符属性

前言

通过上期的讲解,相信大家对操作符已经有了一定的理解,其实学习操作符是为了筑基,学习操作符的根本目的是为了表达式求值,本章就以此目的继续探讨表达式求值!lets go!

一、隐式类型转换

在表达式求值时,有些表达式的操作数在求值的过程中可能需要转换为其他类型,其主要体现为整型提升和算术转换。

1、整型提升

(1)什么是整型提升?

C的整型算术运算总是至少以默认整型类型的精度来进行的。为了获得这个精度,表达式中的char-字符型和short-短整型操作数在使用之前被转换为普通整型,这种转换称为整型提升。

整形提升的条件是:表达式中大小达不到int的charshort才会发生整型提升

(2)为什么会发生整型提升?

表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度一般就是int的字节长度,同时也是CPU的通用寄存器的长度。因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长 度。

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

(3)如何进行整型提升?

整型提升是按照变量的数据类型的符号位来提升的。对于有符号整型提升高位补符号位;对于无符号整型提升,高位补0

📝例如:计算并打印 a+b

#include<stdio.h>
int main()
{
  char a = 5;//截断
  //00000000000000000000000000000101
  //00000101 - a
  char b = 126;//截断
  //00000000000000000000000001111110
  //01111110 - b
  char c = a + b;//整型提升+截断
  //整型提升
  //00000000000000000000000000000101-a
  //00000000000000000000000001111110-b
  //00000000000000000000000010000011
  //10000011 - c
  printf("%d\n", c);
  //%d 十进制的方式打印有符号整数,对c整型提升
  //11111111111111111111111110000011
  //11111111111111111111111110000010
  //10000000000000000000000001111101
  //-125
  return 0;
  //输出:-125
}

(4)小试牛刀

📝1、下面代码输出的结果是什么?

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

📝2、下面代码输出结果是什么?

#include<stdio.h>
int main()
{
    //sizeof返回值是无符号整型,可以用u%打印
  char c = 1;
  printf("%u\n", sizeof(c));
  printf("%u\n", sizeof(+c));
  printf("%u\n", sizeof(-c));
  return 0; 
}
//输出:1 4 4

为什么程序输出结果存在4 呢?正是因为只要char或short存在于表达式中,即使不计算也会发生整型提升,因此会输出4

2、算术转换

(1)寻常算术转换

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

型,否则操作就无法进行。

转换条件:表达式中存在不同类型,并且大于等于int

下面的层次体系称为寻常算术转换:👇

注意:有些转换可能会丢失精度

如:

float f = 3.14;

int num = f;

(2)一道变态的算术转换题

📝下面程序输出结果是什么?

#include <stdio.h>
int i;
int main()
{
    i--;
    if (i > sizeof(i))
    {
        printf(">\n");
    }
    else
    {
        printf("<\n");
    }
    return 0; 
}

解析:C语言中,0为假,非0即为真。全局变量,没有给初始值时,编译其会默认将其初始化为0。i的初始值为0,i- - 结果-1,i为整形,sizeof(i)求i类型大小是4,按照此分析来看,输出结果为<,但是sizeof的返回值类型实际为无符号整形,因此编译器会自动将左侧i自动转换为无符号整形的数据(算术转换),-1对应的无符号整形是一个非常大的数字,超过4或者8,故输出结果为>这道题其实很隐蔽,真是虾仁猪心!!!

二、操作符的属性

1、操作符的三种属性

复杂表达式的求值有三个属性,也就是三个影响的因素:

  1. 操作符的优先级
  2. 操作符的结合性
  3. 是否控制求值顺序。

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

2、操作符属性表

注:

从上往下操作符的优先级越来越低。优先级最高位()最低为

  1. N/A表示不具有结合性;L-R从左向右结合;R-L从右向左结合。
  2. 只有逻辑与-&&、逻辑或-||条件操作符-?:逗号-,操作符控制求值顺序。

补充:控制求值顺序是指,这些操作符在表达式求值时会决定哪些值计算哪些值不计算。

操作符 描述 用法用例 结果类型 结合性 是否控制求值顺序
() 聚组 (表达式) 与表达式相同 N/A
() 函数调用 repx(repx,…,repx) repx L-R
[ ] 下标引用 repx[repx] 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
++ 前缀自增 ++rexp rexp R-L
- - 前缀自减 - -rexp 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

当我们知道了一个表达式的优先级和结合性,我们就可以对复杂表达式进行求值运算。

📝例如:

#include <stdio.h>
int main()
{
  int a, b, c;
  a = 5;
  c = ++a;// ++a:加给a+1,结果为6,用加完之后的结果给c赋值,因此:a = 6  c = 6
  b = ++c, c++, ++a, a++;
  // 逗号表达式的优先级,最低,这里先算b=++c, b得到的是++c后的结果,b是7
  // b=++c 和后边的构成逗号表达式,依次从左向右计算的。
  // 表达式结束时,c++和,++a,a++会给a+2,给c加1,此时c:8,a:8,b:7
  b += a++ + c; // a先和c加,结果为16,在加上b的值7,比的结果为23,最后给a加1,a的值为9
  printf("a = %d b = %d c = %d\n:", a, b, c); // a:9, b:23, c:8
  return 0;
}
//输出:a=9 b=23 c=8

3、问题表达式

通过操作符的属性已经可以解决绝大多数的表达式求值,但是如果操作符使用不当很可能造成问题表达式。

📝问题表达式1

a*b + c*d + e*f

注释:代码1在计算的时候,由于*+的优先级高,只能保证,*的计算是比+早,但是优先级并不能决定第三个*比第一个+早执行。

📝问题表达式2

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(); 中我们只能通过操作符的优先级得知:先算乘法,再算减法。函数的调用先后顺序无法通过操作符的优先级确定。

📝问题表达式3

#include <stdio.h>
int main()
{
  int i = 1;
  int ret = (++i) + (++i) + (++i);
  printf("%d\n", ret);
  printf("%d\n", i);
  return 0; 
}

注释:这段代码中的第一个 + 在执行的时候,第三个++是否执行,这个是不确定的,因为依靠操作符的优先级和结合性是无法决定第一个 + 和第三个前置 ++ 的先后顺序。因此在不同的编译器上运算结果不同。

总结

本章主要介绍了两种隐式类型转换以及操作符属性,重点在于理解并掌握两种转换方式,以及灵活运用操作符属性进行表达式求值。

特别注意:

我们在写表达式时,要保证在求值时计算路径是唯一的。如果我们写出的表达式不能通过操作符的属性确定唯一的计算路径,那这个表达式就是存在问题的。所以请不要折磨编译器,杜绝写出以上华而不实的代码!!!

相关文章
|
1月前
|
编译器 C++
C++系列七:表达式
C++系列七:表达式
|
19天前
运算符与表达式详解
运算符与表达式详解
|
1月前
|
C语言 C++
逗号表达式与赋值表达式
逗号表达式与赋值表达式
17 0
|
1月前
|
SQL 数据库 Python
F表达式
F表达式。
21 4
|
1月前
|
安全 C++ 开发者
c++表达式详细介绍
前言 表达式是 C++ 语言的基石之一,它们在程序中执行计算、赋值、逻辑判断和更多操作。本文旨在提供对 C++ 表达式各个方面的全面了解,包括基础概念、类型、求值规则以及高级主题。
88 0
|
8月前
|
编译器 C语言
操作符的属性,C语言中运算符的优先性和结合性,常见的问题表达式
操作符的属性,C语言中运算符的优先性和结合性,常见的问题表达式
|
10月前
|
编译器 C++
C++的运算符与表达式
在程序中,运算符是用来操作数据的,因此这些数据也被称为操作数,使用运算符将操作数连接而成的式子称为表达式
53 0
C#运算符和表达式的简单运用
C#运算符和表达式的简单运用
|
C# 索引
C#之表达式与运算符
C#之表达式与运算符
|
存储 Unix 编译器
表达式求值过程中会发生哪些隐藏的变化?求值顺序又由什么决定?——详解C表达式求值中的隐式类型转换,算术转换问题,以及操作符的属性
表达式求值过程中会发生哪些隐藏的变化?求值顺序又由什么决定?——详解C表达式求值中的隐式类型转换,算术转换问题,以及操作符的属性
117 0