C语言第10节:详解操作符

发布于:2024-11-04 ⋅ 阅读:(122) ⋅ 点赞:(0)

1. 运算符的分类

运算符是C语言中的基本组成部分,用于执行各种操作。以下是常见运算符的详细分类:

1.1. 算术操作符

这些用于执行基本的数学运算:

  • + :加法
  • - :减法
  • * :乘法
  • / :除法
  • % :取模(只适用于整数)

1.2 关系操作符

这些用于比较两个值,并返回布尔结果(真或假):

  • == :等于
  • != :不等于
  • > :大于
  • < :小于
  • >= :大于或等于
  • <= :小于或等于

1.3 逻辑操作符

这些操作符用于处理逻辑运算:

  • && :逻辑与(AND)
  • || :逻辑或(OR)
  • ! :逻辑非(NOT)

1.4 位操作符

这些用于直接操作二进制位:

  • & :按位与(AND)
  • | :按位或(OR)
  • ^ :按位异或(XOR)
  • ~ :按位取反
  • << :左移
  • >> :右移

1.5 赋值操作符

这些操作符用于将值赋给变量:

  • = :简单赋值
  • += :加并赋值
  • -= :减并赋值
  • *= :乘并赋值
  • /= :除并赋值
  • %= :取模并赋值
  • &= :按位与并赋值
  • |= :按位或并赋值
  • ^= :按位异或并赋值
  • <<= :左移并赋值
  • >>= :右移并赋值

1.6 自增和自减操作符

这些用于增加或减少变量的值,特别是在循环中常用:

  • ++ :自增(可以在变量前或后)
  • -- :自减(可以在变量前或后)

1.7 条件(三元)操作符

这是一个简洁的条件判断方式:

  • ?: :条件操作符,语法为 condition ? expr1 : expr2。如果条件为真,返回 expr1,否则返回 expr2

1.8 指针操作符

这些操作符用于处理指针相关的操作:

  • * :解引用操作符,用于获取指针指向的值
  • & :取地址操作符,用于获取变量的内存地址

1.9 sizeof 操作符

sizeof 用于获得数据类型或变量的字节大小:

  • sizeof :返回指定对象或类型的大小,单位为字节

1.10 逗号操作符

逗号 用于在一行中执行多个操作,返回最后一个操作的值:

  • , :逗号操作符

1.11 类型转换操作符

类型转换操作符用于在不同类型之间转换:

  • (type) :显式类型转换,将变量转换为指定类型

1.12 下标和成员访问操作符

这些操作符用于访问数组、结构体、联合等的成员:

  • [] :下标,用于访问数组元素
  • . :结构体成员访问
  • -> :指向结构体成员(用于结构体指针)

1.13 其他操作符

  • () :函数调用操作符,用于调用函数

2. 二进制和进制转换

在C语言中,理解不同进制系统非常重要,尤其是在处理低级别编程任务时。

十进制中:

  • 10进制中满10进1
  • 10进制的数字每一位都是0~9的数字组成

二进制类似:

  • 2进制中满2进1
  • 2进制的数字每一位都是0~1的数字组成

那么1101 就是二进制的数字了。

2.1 2进制转10进制

在数字系统中,权重(或称位权)是指每一位数字在整体数值中所代表的大小。它是数位系统的基础概念,用来确定每一个数字对总数的贡献。权重的大小取决于它所在的位数和进制。

百位 十位 个位
10进制的位 1 2 3
权重 10^2 10^1 10^0
权重值 100 10 1
求值 1*100 + 2*10 + 3*1 123

2进制和10进制是类似的,只不过2进制的每一位的权重不同,这里以1101为例:

2进制的位 1 1 0 1
权重 2^3 2^2 2^1 2^0
权重值 8 4 2 1
求值 1*8 + 1*4 + 0*2 + 1*1 = 13

2.1.1 10进制转2进制

对于整数的十进制转二进制转换,通常使用“除2取余法”:

  1. 将数除以2,记录余数。
  2. 继续除2,直到商为0。
  3. 最后余数逆序排列即为该数的二进制表示。

十进制到二进制转换步骤(这里以125为例)

除数 被除数 余数
2 125 1
2 62 0
2 31 1
2 15 1
2 7 1
2 3 1
2 1 1
2 0
由下往上依次排列余数

10进制的125转换的2进制:1111101

2.2 2进制转8进制和16进制

二进制与八进制、十六进制之间的转换较为方便,因为它们都是2的幂。

2.2.1 二进制转八进制

八进制的每一位可以用 3 位二进制数表示,因为2^3 =8。也就是说,每三位二进制数可以对应一个八进制数字。

转换步骤:
  1. 将二进制数从右到左分组,每三位为一组。如果最后一组不满三位,可以在前面补 0。
  2. 将每一组三位的二进制数转换成相应的八进制数字
  3. 按照分组顺序,将得到的八进制数字组合起来,就是最终的八进制数。
示例:

我们以二进制数 11011101 为例,将其转换为八进制。

  1. 从右到左,每 三位一组分组:110 111 01
  2. 如果最后一组不足三位,在前面补 0,得到:011 011 101
  3. 将每组三位转换为对应的八进制数:
    • 011 = 3
    • 011 = 3
    • 101 = 5
  4. 将这些八进制数字组合起来:335

所以,二进制数 11011101 转换为八进制是 335

在这里插入图片描述

2.2.2 二进制转十六进制

十六进制的每一位可以用 4 位二进制数表示,因为 2^4 = 16。也就是说,每四位二进制数可以对应一个十六进制数字。

转换步骤:
  1. 将二进制数从右到左分组,每四位为一组。如果最后一组不满四位,可以在前面补 0。
  2. 将每组四位的二进制数转换为相应的十六进制数字
  3. 按照分组顺序,将得到的十六进制数字组合起来,就是最终的十六进制数。
示例:

将二进制数 11011101 转换为十六进制。

  1. 从右到左分组:1101 1101。每组正好四位,不需要补 0。
  2. 每组四位转换为一个十六进制数:
    • 1101 = D
    • 1101 = D
  3. 将这些数字组合起来:DD

所以,11011101 的十六进制表示是 DD

在这里插入图片描述

3. 原码、反码、补码

我们再来聊一聊原码、反码和补码

在计算机科学中,原码反码补码是用来表示整数的不同编码方式,尤其是在表示负数时非常有用。这些编码主要应用于二进制的计算机系统,下面详细讲解它们的概念和区别:

3.1 原码(Sign-Magnitude)

定义:原码是整数的二进制直接表示方法。使用最高位(最左边的一位)表示符号,其余位表示数值的大小。最高位为 0 表示正数,1 表示负数。

表示方法

  • 正数:直接将数值的二进制表示。
  • 负数:最高位设置为 1,其余位仍然是数值的二进制表示。

示例

  • +5 的原码(假设 8 位):0000 0101
  • -5 的原码:1000 0101

优缺点

  • 优点:表示直观,符号位直接区分正负。
  • 缺点:存在两个零(+0 和 -0),会导致一些计算和比较操作复杂化。

3.2 反码(Ones’ Complement)

定义:反码是对原码的进一步改进。在反码表示中,正数的反码与原码相同,而负数的反码是将该数的二进制位逐位取反(即 0110)。

表示方法

  • 正数:与原码相同。
  • 负数:将正数的每一位取反。

示例

  • +5 的反码(8 位):0000 0101
  • -5 的反码:1111 1010

优缺点

  • 优点:比原码更接近现代计算机的计算方式,逻辑上容易实现。
  • 缺点:仍然存在两个零(+0 和 -0):0000 00001111 1111。这也会导致一些计算上的不便,比如减法操作需要特殊处理。

3.3 补码(Two’s Complement)

定义:补码是现代计算机中广泛采用的一种编码方式,可以解决原码和反码中两个零的问题。在补码表示中,负数的补码是反码加 1

表示方法

  • 正数:与原码和反码相同。
  • 负数:取反码后加 1

计算方法

  • 将一个负数的绝对值转成二进制。
  • 求出该数的反码。
  • 反码加 1 即为补码。

示例

  • +5 的补码(8 位):0000 0101
  • -5 的补码:先取反码 1111 1010,然后加 11111 1011

优缺点

  • 优点:只有一个零,表示负数非常方便,并且加减法可以统一使用补码,不需要特别处理减法。
  • 缺点:在表示范围上会有不对称的情况。例如对于 8 位补码,最大值是 +127,最小值是 -128

3.4 为什么补码是主流表示法?

补码表示负数的方式有几个重要的优点,使得它在现代计算机中成为主流:

  1. 单一零点:补码只有一个零(0000 0000),解决了原码和反码中存在两个零的问题,简化了判断和计算逻辑。
  2. 统一的加减法运算:补码使得减法运算可以通过加上负数来实现,无需额外的硬件电路。这简化了硬件设计,也提升了计算效率。
  3. 对称的溢出规则:在补码中,溢出会自动回到最小或最大表示范围。例如,8 位补码中的 +128 溢出时会变为 -128。这种性质在循环计数等场景中很有用。
数字 原码(8位) 反码(8位) 补码(8位)
+5 0000 0101 0000 0101 0000 0101
-5 1000 0101 1111 1010 1111 1011
+0 0000 0000 0000 0000 0000 0000
-0 1000 0000 1111 1111 0000 0000(不存在负零)

3.5 总结

  • 原码:最高位为符号位,正数不变,负数符号位为 1,其余位为数值。
  • 反码:正数与原码相同,负数将原码中的所有位取反。
  • 补码:正数与原码相同,负数为反码加 1,解决了多零的问题,是计算机中常用的负数表示方法。

4. 移位操作符

C语言中的移位运算符用于将数的二进制位左移或右移,这些操作可以用于快速实现乘法或除法的效果,或者用于位操作。C语言中的移位运算符有两个:

  • << 左移运算符
  • >> 右移运算符

4.1 左移运算符 <<

基本语法

result = value << n;
  • value 是要被左移的值。
  • n 是要左移的位数。
  • result 是操作后的结果。

功能

左移运算符会将 value 的二进制位向左移动 n 位,右边补上 n 个0。左移相当于对数值乘以 2^n

示例

假设 value = 3(二进制表示为 00000011),如果 value << 2,结果为:

  • 二进制:00000011 << 2 变成 00001100
  • 十进制:3 * 2^2 = 12

注意事项

  • 左移会丢弃超出数值类型位宽的高位部分。
  • 如果 value 是负数,左移的行为可能会依赖于具体实现(一般在无符号整数中更常用)。

4.2 右移运算符 >>

基本语法

result = value >> n;
  • value 是要被右移的值。
  • n 是要右移的位数。
  • result 是操作后的结果。

功能

右移运算符将 value 的二进制位向右移动 n 位,如果 value 是正数,则高位用 0 补位(逻辑右移);如果 value 是负数,补位行为可能因编译器不同而有所差异(算术右移或逻辑右移)。

示例

假设 value = 12(二进制表示为 00001100),如果 value >> 2,结果为:

  • 二进制:00001100 >> 2 变成 00000011
  • 十进制:12 / 2^2 = 3

注意事项

  • 对于无符号数,右移操作始终在左边填充 0
  • 对于有符号数,右移可能是算术右移(保留符号位)或逻辑右移(高位补0),这在不同平台上可能有所不同。

4.3 实际应用

  1. 快速乘法和除法:左移相当于乘以 2^n,右移相当于除以 2^n。例如:

    int x = 5;
    int result = x << 1; // 相当于 5 * 2 = 10
    
  2. 位掩码:可以用左移或右移来生成特定的掩码。例如,如果我们希望生成一个数的某个特定位为 1,可以通过移位操作实现:

    int mask = 1 << 3; // 生成掩码 00001000,将第四位设置为1
    
  3. 提取和设置特定位:通过移位和按位操作,可以方便地提取和修改特定的位。例如,检查某个数的第三位是否为 1

    int x = 9;          // 二进制:00001001
    int check = x & (1 << 3); // 检查第三位
    

移位操作的注意事项

  1. 溢出问题:左移操作可能会引起溢出。比如对一个8位的数左移超过8次可能会导致结果不可预测。
  2. 类型限制:移位操作的最大位数不应超过数据类型的位宽。例如,对于 int 类型,移位的位数不应超过 31 位。
  3. 有符号数右移的实现差异:在一些编译器中,有符号数的右移是算术右移(保持符号位),而在另一些编译器中可能是逻辑右移(补 0),这取决于实现。

总结来说,移位运算符在高效运算和低级数据操作中非常有用。理解移位操作可以帮助更好地处理位操作、优化性能。

5. 位操作符:&、|、^、~

在C语言中,位操作符用于直接操作数的二进制位,这在硬件编程、性能优化和数据处理领域尤其常用。以下是每个位操作符的详细解释和示例:

1. 按位与(&)

按位与运算符 & 会对两个数的每一位进行“与”运算。只有当对应的位都是 1 时,结果才为 1;否则结果为 0

用法
result = a & b;
示例

假设 a = 6(二进制 00000110)和 b = 3(二进制 00000011),则 a & b 的结果为:

  • 二进制:00000110 & 00000011 = 00000010
  • 十进制:2
应用
  • 位掩码:按位与常用于清除(屏蔽)某些位。例如,将某数的某些位清零。
  • 奇偶判断:可以用按位与 1 来检查数的最后一位,从而判断奇偶数。例如 x & 1,如果结果为 0,则 x 是偶数,否则是奇数。
示例代码
int x = 5;       // 二进制:0101
int y = 3;       // 二进制:0011
int result = x & y; // 结果:0001 (十进制为1)

2. 按位或(|)

按位或运算符 | 会对两个数的每一位进行“或”运算。只要对应的位中有一个 1,结果就是 1;如果两个位都是 0,结果才为 0

用法
result = a | b;
示例

假设 a = 6(二进制 00000110)和 b = 3(二进制 00000011),则 a | b 的结果为:

  • 二进制:00000110 | 00000011 = 00000111
  • 十进制:7
应用
  • 设置位:按位或可用于将特定位设置为 1
  • 组合位掩码:通过按位或,可以将多个掩码组合在一起。
示例代码
int x = 5;       // 二进制:0101
int y = 3;       // 二进制:0011
int result = x | y; // 结果:0111 (十进制为7)

3. 按位异或(^)

按位异或运算符 ^ 会对两个数的每一位进行“异或”运算。若对应的位不同,则结果为 1;若相同,则结果为 0

用法
result = a ^ b;
示例

假设 a = 6(二进制 00000110)和 b = 3(二进制 00000011),则 a ^ b 的结果为:

  • 二进制:00000110 ^ 00000011 = 00000101
  • 十进制:5
应用
  • 位翻转:按位异或可用于翻转指定的位。
  • 交换变量值:通过一系列异或操作可以实现变量值的交换,而无需额外的临时变量。
  • 校验和运算:在一些简单的加密算法和校验和算法中,按位异或常用于数据混淆。
示例代码
int x = 5;       // 二进制:0101
int y = 3;       // 二进制:0011
int result = x ^ y; // 结果:0110 (十进制为6)
使用异或实现变量交换
int a = 5, b = 3;
a = a ^ b; // a 变成了 6 (0101 ^ 0011 = 0110)
b = a ^ b; // b 变成了 5 (0110 ^ 0011 = 0101)
a = a ^ b; // a 变成了 3 (0110 ^ 0101 = 0011)

4. 按位取反(~)

按位取反运算符 ~ 是一个一元操作符,只对一个操作数进行操作,将数的每一位都取反。即:0 变为 11 变为 0

用法
result = ~a;
示例

假设 a = 6(二进制 00000110),则 ~a 的结果为:

  • 二进制:~00000110 = 11111001
  • 由于二进制按补码表示,11111001 代表 -7(补码的负数表示)。
应用
  • 快速计算负数:在补码表示中,~a + 1 就是 -a
  • 位掩码反转:在一些低级操作中,按位取反可以用于生成特定的位掩码。
示例代码
int x = 5;       // 二进制:00000101
int result = ~x; // 结果:11111010 (十进制为-6)

小结

操作符 描述 例子(a = 6, b = 3) 二进制计算 结果
& 按位与 a & b 00000110 & 00000011 2
` ` 按位或 `a b`
^ 按位异或 a ^ b 00000110 ^ 00000011 5
~ 按位取反 ~a ~00000110 -7

一道变态的面试题:

不能创建临时变量(第三个变量),实现两个整数的交换。

#include <stdio.h>
int main()
{
	int a = 10;
	int b = 20;
	a = a^b;
	b = a^b;
	a = a^b;
	printf("a = %d b = %d\\n", a, b);
	return 0;
}

**练习1:**编写代码实现:求一个整数存储在内存中的二进制中1的个数。

参考代码:
    //方法1
    #include <stdio.h>
    int main()
{
    int num = 10;
    int count= 0;//计数
    while(num)
    {
        if(num%2 == 1)
            count++;
        num = num/2;
    }
    printf("二进制中1的个数 = %d\\n", count);
    return 0;
}
//思考这样的实现方式有没有问题?
//方法2:
#include <stdio.h>
int main()
{
    int num = -1;
    int i = 0;
    int count = 0;//计数
    for(i=0; i<32; i++)
    {
        if( num & (1 << i) )
            count++;
    }
    printf("二进制中1的个数 = %d\\n",count);
    return 0;
}
//思考还能不能更加优化,这里必须循环32次的。
//方法3:
#include <stdio.h>
int main()
{
    int num = -1;
    int i = 0;
    int count = 0;//计数
    while(num)
    {
        count++;
        num = num&(num-1);
    }
    printf("二进制中1的个数 = %d\\n",count);
    return 0;
}
//这种方式是不是很好?达到了优化的效果,但是难以想到。

** 练习2:**二进制位置0或者置1

编写代码将13二进制序列的第5位修改为1,然后再改回0

132进制序列: 00000000000000000000000000001101
将第5位置为1后:00000000000000000000000000011101
将第5位再置为000000000000000000000000000001101

参考代码:

#include <stdio.h>
int main()
{
    int a = 13;
    a = a | (1<<4);
    printf("a = %d\\n", a);
    a = a & ~(1<<4);
    printf("a = %d\\n", a);
    return 0;
}

6. 单目操作符

单目操作符有这些:

!、++、--、&、*、+、-、~ 、sizeof、(类型)

单目操作符的特点是只有一个操作数。其他的已经学过,&*在后面学习

7. 逗号表达式

exp1, exp2, exp3, …expN

逗号表达式允许在一个语句中执行多个表达式,从左到右依次求值。逗号表达式的返回值是最后一个表达式的值。

示例:

int a = (1, 2, 3);  // a = 3,因为最后一个表达式是3
int b = 0;
b = (a++, ++a, a + 1); // a先自增,a再自增,最终a+1的值赋给b,b = 6

逗号表达式常用于for循环中的初始化或条件语句。例如:

for (int i = 0, j = 10; i < j; i++, j--) {
    printf("i: %d, j: %d\n", i, j);
}

8. 下标访问和函数调用 ()

8.1 [] 下标引用运算符

下标运算符 [] 用于访问数组的元素。表达式 array[i] 表示数组 array 中下标为 i 的元素。

示例:

int array[5] = {1, 2, 3, 4, 5};
int element = array[2]; // element = 3

8.2 函数调用运算符 ()

函数调用运算符 () 用于调用函数,将一系列参数传递给函数并获得返回值。

示例:

#include <stdio.h>

int add(int x, int y) {
    return x + y;
}

int main() {
    int result = add(3, 5);  // 调用add函数
    printf("result = %d\n", result); // 输出result = 8
    return 0;
}

9. 结构成员访问运算符

在C语言中,结构体(struct) 是一种用户定义的复合数据类型,它允许我们将不同类型的数据组合在一起。结构体在需要将多个相关变量归组为一个整体时非常有用,比如描述一个学生的属性(姓名、年龄、成绩等)。

9.1 结构体的声明和初始化

9.1.1 结构体声明

结构体的声明使用关键字 struct,并在大括号 {} 内定义成员变量。每个成员可以是不同的数据类型,例如 intfloat 或者其他自定义类型。

示例:
struct Student {
    char name[50];   // 学生姓名
    int age;         // 学生年龄
    float gpa;       // 学生成绩
};

在这个示例中,我们声明了一个结构体 Student,包含以下三个成员:

  • name:字符数组,用于存储学生姓名。
  • age:整数,用于存储学生年龄。
  • gpa:浮点数,用于存储学生的GPA。

9.1.2 结构体变量的定义和初始化

在声明结构体后,我们可以定义结构体变量,并对其成员进行初始化。可以在定义时直接赋初值,也可以在之后分别赋值。

直接初始化:
struct Student student1 = {"Alice", 20, 3.8};
分步赋值:
struct Student student2;
strcpy(student2.name, "Bob"); // 给name赋值
student2.age = 22;            // 给age赋值
student2.gpa = 3.5;           // 给gpa赋值

在分步赋值中,我们可以分别为结构体的每个成员赋值。例如这里使用 strcpy 函数为 name 赋值,因为 name 是一个字符数组,不能直接使用 = 赋值。

9.2 结构成员访问运算符

结构体成员访问运算符有两种:点运算符(.箭头运算符(->

  • 点运算符(.:用于访问结构体变量的成员。
  • 箭头运算符(->:用于访问结构体指针变量的成员。(后面学了指针再说)

9.2.1 结构体成员的直接访问

如果我们定义了一个结构体变量,可以通过**点运算符 .**直接访问其成员。

示例:
struct Student student1 = {"Alice", 20, 3.8};

// 访问并打印结构体成员
printf("Name: %s\n", student1.name);   // 输出:Name: Alice
printf("Age: %d\n", student1.age);     // 输出:Age: 20
printf("GPA: %.2f\n", student1.gpa);   // 输出:GPA: 3.80

在这里,student1.name 表示结构体 student1name 成员,student1.age 表示 age 成员,以此类推。

9.2.2 结构体成员的间接访问

9.3 结构体数组

我们可以声明结构体数组来存储多个结构体变量,每个数组元素都是一个结构体。例如,用于存储多个学生的信息。

示例:
struct Student students[3] = {
    {"Alice", 20, 3.8},
    {"Bob", 22, 3.5},
    {"Charlie", 19, 3.9}
};

// 访问结构体数组的成员
for (int i = 0; i < 3; i++) {
    printf("Student %d:\n", i + 1);
    printf("  Name: %s\n", students[i].name);
    printf("  Age: %d\n", students[i].age);
    printf("  GPA: %.2f\n", students[i].gpa);
}

在此示例中,我们声明了一个结构体数组 students,每个元素都是一个 Student 结构体。我们可以使用 students[i].member 的方式来访问每个结构体变量的成员。

9.4 结构体指针作为函数参数

9.5 嵌套结构体

结构体可以嵌套,即在一个结构体中包含另一个结构体。常用于描述复杂的数据结构。

示例:
struct Address {
    char city[50];
    int postalCode;
};

struct Student {
    char name[50];
    int age;
    struct Address address;  // 嵌套的结构体
};

int main() {
    struct Student student1 = {"Alice", 20, {"New York", 10001}};
    
    // 访问嵌套结构体的成员
    printf("Name: %s\n", student1.name);
    printf("Age: %d\n", student1.age);
    printf("City: %s\n", student1.address.city);
    printf("Postal Code: %d\n", student1.address.postalCode);
    
    return 0;
}

在这个示例中,Student 结构体中包含了另一个结构体 Address,我们可以通过 student1.address.city 的方式访问嵌套结构体的成员。

9.6 结构体的应用

结构体在C语言中有很多实际应用,包括但不限于以下情况:

  • 管理复杂数据:如存储学生信息、商品信息、员工信息等。
  • 作为函数参数:传递结构体数据到函数中进行操作。
  • 使用结构体数组:批量管理多个实体(如学生、员工等)的信息。
  • 数据嵌套:通过嵌套结构体实现复杂的数据结构,例如描述一个城市的多个属性(人口、面积、经济等)。

10. 运算符的属性:优先级和结合性

C语言的操作符有2个重要的属性:优先级、结合性,这两个属性决定了表达式求值的计算顺序。

10.1 优先级

运算符的优先级决定了表达式中各个运算的执行顺序。例如,乘法和除法运算的优先级高于加法和减法。

示例:

int result = 3 + 4 * 5;  // 结果是23,而不是35,因为乘法优先级高于加法

常见运算符的优先级(从高到低):

  1. 括号 ()
  2. 自增、自减 ++--,单目运算符 +-!~
  3. 乘法、除法、取模 */%
  4. 加法、减法 +-
  5. 位移运算 <<>>
  6. 关系运算符 <<=>>=
  7. 相等运算符 ==!=
  8. 位运算符 &^|
  9. 逻辑运算符 &&||
  10. 赋值运算符 =+=-=
  11. 逗号 ,

10.2 结合性

当表达式中出现多个优先级相同的运算符时,结合性决定了运算的顺序。结合性可以是从左到右,也可以是从右到左。

示例:

int a = 5, b = 10, c = 15;
int result = a + b - c;  // 左结合性,从左到右计算:result = (a + b) - c

在这里插入图片描述

C 运算符优先级

11. 表达式求值

11.1 整型提升

整型提升是指在表达式中,低精度的整型数据会自动转换为更高精度的整型,以避免数据丢失。例如,charshort 在进行计算时会自动转换为 int 类型。

整型提升的意义:

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

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

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

示例:

char a = 5;
int b = 10;
int result = a + b;  // a 会自动提升为 int 类型

如何进行整体提升呢?

  1. 有符号整数提升是按照变量的数据类型的符号位来提升的
  2. 无符号整数提升,高位补0

11.2 算术转换

当表达式中存在不同数据类型时,C语言会自动进行类型转换以保证运算的兼容性。这种转换规则称为“算术转换”,通常按从低到高精度的顺序转换。

例如,floatint 进行运算时,int 会被提升为 float

int a = 5;
float b = 2.5;
float result = a + b;  // a 会转换为 float,result = 7.5

11.3 问题表达式的解析

解析复杂表达式时,需要考虑优先级和结合性,逐步分解表达式。

11.3.1 表达式示例1

//表达式的求值部分由操作符的优先级决定。
//表达式1
a*b + c*d + e*f

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

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

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

或者

a*b
c*d
e*f

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

11.3.2 表达式示例2

//表达式2
c + --c;

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

11.3.3 表达式3

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

表达式3在不同编译器中测试结果:表达式程序的结果

在这里插入图片描述

11.4 总结

C语言中运算符的优先级和结合性非常重要,直接影响表达式的求值顺序。熟练掌握这些规则,可以帮助避免代码中的潜在错误,尤其是在复杂表达式中,使用括号明确表达式的优先级是良好的编程习惯。但是即使有了操作符的优先级和结合性,我们写出的表达式依然有可能不能通过操作符的属性确定唯一的计算路径,那这个表达式就是存在潜在风险的,建议不要写出特别复杂的表达式。

—完—


网站公告

今日签到

点亮在社区的每一天
去签到