• 表达式求值过程中会发生哪些隐藏的变化?求值顺序又由什么决定?——详解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 
    
    int main()
    {
    	unsigned char a = -1;
    	short b = 1;
    	printf("%d", a + b);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    大家思考思考,会不会发生整型提升,结果是啥,是-1+1=0吗
    好,我们一起来分析一下:

    在这里插入图片描述

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

    在这里插入图片描述
    实例二:

    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    这道题的结果又是啥呢?
    在这里插入图片描述

    看看结果:

    在这里插入图片描述
    只打印了c。

    实例三:

    int main()
    {
     char c = 1;
     printf("%u\n", sizeof(c));
     printf("%u\n", sizeof(+c));
     printf("%u\n", sizeof(-c));
     return 0; }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    我们分析结果应该是 1,4,4;
    c只要参与表达式运算,就会发生整形提升,表达式 +c ,发生整型提升,所以 sizeof(+c) 是4个字节(一个整型大小).
    同理 sizeof(-c) 也是4个字节,
    但是 sizeof( c ) ,就是1个字节,因为没有发生整型提升,还是char类型。

    在这里插入图片描述

    二.算术转换

    1.什么是算术转换

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

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

    下面的层次体系称为寻常算术转换。
    在这里插入图片描述

    2.举例

    #include 
    int main()
    {
    	int a = 10;
    	double b = 22.2;
    	printf("%d", sizeof(a + b));
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    这段代码会不会发生算术转换,结果是什么呢?
    我们来分析一下:

    在这里插入图片描述
    我们看看结果:
    在这里插入图片描述

    三.操作符的属性

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

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

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

    那它们如何取影响表达式的求值顺序呢?
    (1)两个相邻的操作符先执行哪个?取决于他们的优先级。
    (2)如果两者的优先级相同,取决于他们的结合性。

    在这里插入图片描述

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

    举例说明一下:
    在这里插入图片描述
    如果优先级相同:
    在这里插入图片描述
    然后给大家解释一下控制求值顺序是什么意思吧!

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

    其实就是rexp1可以决定rexp2,rexp3,哪一个表达式先算,哪一个后算.
    因为我们知道他的运算规律是:
    如果rexp1成立,执行rexp2,整个表达式的结果是rexp2的结果;
    如果rexp1不成立,执行rexp3,整个表达式的结果是rexp3的结果

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

    2.问题表达式

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

    a*b + c*d + e*f
    
    • 1

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

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

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

    ab
    c
    d
    ab + cd
    ef
    a
    b + cd + ef

    或者

    ab
    c
    d
    ef
    a
    b + cd
    a
    b + cd + ef

    再来看一个:

    c + --c;
    
    • 1

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

    比如:
    在这里插入图片描述

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

    再来一个:

    int main()
    {
     int i = 10;
     i = i-- - --i * ( i = -3 ) * i++ + ++i;
     printf("i = %d\n", i);
     return 0; }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    代码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 
    int main()
    {
     int i = 1;
     int ret = (++i) + (++i) + (++i);
     printf("%d\n", ret);
     printf("%d\n", i);
     return 0; }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

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

    尝试在linux 环境gcc编译器,VS环境下都执行,看结果。

    在这里插入图片描述

    vs环境下:

    在这里插入图片描述

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

    3.总结

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

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

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

    以上就是对表达式求值的一些问题讲解,欢迎大家指正!!!
    在这里插入图片描述

  • 相关阅读:
    AI绘图—对中文拟合度很高,值得一试
    CentOS 7 服务器上创建新用户及设置用户密码有效期
    SpringCloud 微服务全栈体系(十五)
    E. Draw a triangle(计算几何+EXGCD)
    【数独 2】候选数法解数独谜题-挖掘更深的信息-C++实现
    洛谷 P1016 [NOIP1999 提高组] 旅行家的预算【贪心】
    ardupilot开发 --- Custom Attitude Controller 篇
    ARP 地址解析协议——IP地址到MAC地址
    Python-Numpy中的repmat
    深入Spring Boot :web.xml去哪了
  • 原文地址:https://blog.csdn.net/m0_70980326/article/details/126471557