表达式求值过程中会发生哪些隐藏的变化?求值顺序又由什么决定?——详解C表达式求值中的隐式类型转换,算术转换问题,以及操作符的属性

简介: 表达式求值过程中会发生哪些隐藏的变化?求值顺序又由什么决定?——详解C表达式求值中的隐式类型转换,算术转换问题,以及操作符的属性

我们写出的表达式,在求值的过程中,一定是按照我们所想的在一步一步运算吗?会不会发生一些我们察觉不到的变化呢?任意给定一个表达式,它的计算路径一定是确定的吗?


这篇文章,将带领大家深入的讨论和学习这些问题,一起来看看吧!



表达式求值

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

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


一.隐式类型转换——整型提升

1.什么是整型提升呢?

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

为了获得这个精度,表达式中的字符和短整型(所占空间大小小于一个整型的大小)操作数在使用之前被转换为普通整型,这种转换称为整型提升。


比如:


char a,b,c;

a = b + c;


这里就会发生整型提升:


(1) b和c的值被提升为普通整型,然后再执行加法运算。

(2) 加法运算完成之后,结果将被截断,然后再存储于a中。


2.那如何进行整型提升呢?按照什么规则?

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


(1)负数的整形提升(高位补符号位1)


char c1 = -1;

变量c1的二进制位(补码)中只有8个比特位:

1111111

因为 char 为有符号的 char

所以整形提升的时候,高位补充符号位,即为1

提升之后的结果是:

11111111111111111111111111111111


(2)正数的整形提升(高位补符号位0)


char c2 = 1;

变量c2的二进制位(补码)中只有8个比特位:

00000001

因为 char 为有符号的 char

所以整形提升的时候,高位补充符号位,即为0

提升之后的结果是:

00000000000000000000000000000001


(3)无符号整形提升,高位补0


unsigned char c1 = -1;

变量c1的二进制位(补码)中只有8个比特位:

1111111

因为 char 为无符号的 char

所以整形提升的时候,高位补0

提升之后的结果是:

00000000000000000000000011111111


3.实战演练

好,了解了规则之后,我们就来举几个例子吧。

实例一:

#include <stdio.h>
int main()
{
  unsigned char a = -1;
  short b = 1;
  printf("%d", a + b);
  return 0;
}

大家思考思考,会不会发生整型提升,结果是啥,是-1+1=0吗

好,我们一起来分析一下:

5b0a33b3263d487f8d4c153bc1c9f793.png

根据我们的分析答案是256。我看运行看看是不是:

b9a3c7576ad4406281765d52b61633b6.png

实例二:

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

这道题的结果又是啥呢?

b2a0a9b491ec4a92807bdb822063f388.png

看看结果:

3b5840c586314efa84af935782776e52.png

只打印了c。

实例三:

int main()
{
 char c = 1;
 printf("%u\n", sizeof(c));
 printf("%u\n", sizeof(+c));
 printf("%u\n", sizeof(-c));
 return 0; }

我们分析结果应该是 1,4,4;

c只要参与表达式运算,就会发生整形提升,表达式 +c ,发生整型提升,所以 sizeof(+c) 是4个字节(一个整型大小).

同理 sizeof(-c) 也是4个字节,

但是 sizeof( c ) ,就是1个字节,因为没有发生整型提升,还是char类型。

608afbdb5277485496adaf9bf38634ee.png

二.算术转换

1.什么是算术转换

然后我们来学习算术转换,那什么是算术转换呢?

算术转换:

如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类型,否则操作就无法进行。

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

f7f6ca3d355146c48620bd9e1f7964c2.png

2.举例

#include <stdio.h>
int main()
{
  int a = 10;
  double b = 22.2;
  printf("%d", sizeof(a + b));
  return 0;
}

这段代码会不会发生算术转换,结果是什么呢?

我们来分析一下:4f2b241c653e4ae1b6226fa3b40a1502.png

我们看看结果:

80e3cb5481194d948f93abb73bf7252c.png

三.操作符的属性

1.操作符如何控制表达式求值

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


1. 操作符的优先级

2. 操作符的结合性

3. 是否控制求值顺序


那它们如何取影响表达式的求值顺序呢?

(1)两个相邻的操作符先执行哪个?取决于他们的优先级。

(2)如果两者的优先级相同,取决于他们的结合性。

9b52985a8ba34761812cf0baaa124130.png

注意:N/A是空的意思,R/L是从右向左的意思,L/R是从左向右的意思。

举例说明一下:

57cd8069c8e0478badb917ff8bbdfb60.png

如果优先级相同:

3a3b75be97894642a41a59e6d2cbfe68.png

然后给大家解释一下控制求值顺序是什么意思吧!


举个例子,我们看到上面表格中的 rexp1? rexp2:rexp3 是控制求值顺序的。


其实就是rexp1可以决定rexp2,rexp3,哪一个表达式先算,哪一个后算.

因为我们知道他的运算规律是:

如果rexp1成立,执行rexp2,整个表达式的结果是rexp2的结果;

如果rexp1不成立,执行rexp3,整个表达式的结果是rexp3的结果


所以说它可以控制求值顺序。


2.问题表达式

接下来我们一起来看一些表达式,大家判断一下它们的求值顺序:


a*b + c*d + e*f

1

这其实是一个问题表达式,为啥呢?


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


所以表达式的计算机顺序就可能是:


ab

cd

ab + cd

ef

ab + cd + ef


或者


ab

cd

ef

ab + cd

ab + cd + ef


再来看一个:

c + --c;

同样存在一些问题:

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

比如:

a3b57cba9a2745b09a95648206c08b4e.png

在不同的编译器上,结果就可能不一致!!!


再来一个:

int main()
{
 int i = 10;
 i = i-- - --i * ( i = -3 ) * i++ + ++i;
 printf("i = %d\n", i);
 return 0; }

代码3在不同编译器中的测试结果:


值 编译器

—128 Tandy 6000 Xenix 3.2

—95 Think C 5.02(Macintosh)

—86 IBM PowerPC AIX 3.2.5

—85 Sun Sparc cc(K&C编译器)

—63 gcc,HP_UX 9.0,Power C 2.0.0

4 Sun Sparc acc(K&C编译器)

21 Turbo C/C++ 4.5

22 FreeBSD 2.1 R

30 Dec Alpha OSF1 2.0

36 Dec VAX/VMS

42 Microsoft C 5.1

#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编译器,VS环境下都执行,看结果。

22d732dc51f54adf8e03569847f4712f.png

vs环境下:

da59cae748724f1baa6e85ab280b68ac.png

这些都是有问题的表达式,我们在写代码的过程中,要避免写这样的代码!!!


3.总结

上面的问题表达式告诉我们:


即使我们知道了所有操作符的优先级和结合性,以及其是否控制求值顺序,我们也不能保证任意写一个表达式,它的求值顺序就一定是确定的,我们写出的表达式如果不能通过操作符的属性确定唯一的计算路径,那这个表达式就是存在问题的。


所以,对于比较复杂的代码我们可以拆开写,养成良好的代码风格,避免写出这样不好的代码!!!


以上就是对表达式求值的一些问题讲解,欢迎大家指正!!!

76d0e0f1c3c040ecabdb5e5f1ce7afa7.png

目录
相关文章
|
7月前
|
C语言 C++
操作符的属性:优先级、结合性(缺表达式求值)
操作符的属性:优先级、结合性(缺表达式求值)
50 0
|
7月前
|
存储 编译器 C语言
【表达式求值】整型提升和算术转换
【表达式求值】整型提升和算术转换
56 0
|
1月前
|
数据处理 Swift
Swift 中的运算符和表达式是构建程序逻辑的基础,包括算术、关系、逻辑、位运算符及赋值运算符,用于数值计算、条件判断、位操作、赋值与更新等
Swift 中的运算符和表达式是构建程序逻辑的基础,包括算术、关系、逻辑、位运算符及赋值运算符,用于数值计算、条件判断、位操作、赋值与更新等。掌握这些工具是编写高效代码的关键。
25 1
|
7月前
|
C#
赋值组合运算符
赋值组合运算符
41 1
|
7月前
|
编译器 测试技术 Go
表达式求值——隐式类型转换与操作符属性
表达式求值——隐式类型转换与操作符属性
|
7月前
|
编译器 C++ 索引
C learning_13 操作符前篇(条件操作符、 逗号表达式、 下标引用、函数调用和结构成员、 表达式求值)
C learning_13 操作符前篇(条件操作符、 逗号表达式、 下标引用、函数调用和结构成员、 表达式求值)
|
编译器 C语言
操作符的属性,C语言中运算符的优先性和结合性,常见的问题表达式
操作符的属性,C语言中运算符的优先性和结合性,常见的问题表达式
|
编译器 C语言
【C语言初阶】带你轻松玩转所有常用操作符(3)——关系操作符,逻辑操作符,条件操作符,逗号表达式
【C语言初阶】带你轻松玩转所有常用操作符(3)——关系操作符,逻辑操作符,条件操作符,逗号表达式
118 0
【C语言初阶】带你轻松玩转所有常用操作符(3)——关系操作符,逻辑操作符,条件操作符,逗号表达式
隐式类型转换 算术转换 操作符的属性
隐式类型转换 算术转换 操作符的属性
62 0