19 C 语言位运算、赋值、条件、逗号运算符详解:涵盖运算符优先级与复杂表达式计算过程分析

1 位运算符

位运算符是对整数的二进制表示(补码形式)进行逐位操作的运算符。以下是主要的位运算符及其功能描述:

运算符 描述 操作数个数 副作用
& 按位与 2
| 按位或 2
^ 按位异或 2
~ 按位取反 1
<< 按位左移 2
>> 按位右移 2

1.1 按位与 &

  • 运算规则: 对于两个数的每一位,如果两个相应的位都为 1,则结果的该位为 1;否则,结果的该位为 0(遵循 "有 0 为 0,全 1 为 1" 规则)。
  • 示例: 5 & 3
    • 5 的二进制表示:0101
    • 3 的二进制表示:0011
    • 按位与操作过程:
bash 复制代码
    0 1 0 1  
&  
    0 0 1 1  
    -------  
    0 0 0 1

结果:0001(十进制1)

1.2 按位或 |

  • 运算规则: 对于两个数的每一位,如果两个相应的位中至少有一个为 1,则结果的该位为 1;如果两个相应的位都为 0,则结果的该位为 0(遵循 "有 1 为 1,全 0 为 0" 规则)。
  • 示例: 5 | 3
    • 5 的二进制表示:0101
    • 3 的二进制表示:0011
    • 按位或操作过程:
bash 复制代码
    0 1 0 1  
|  
    0 0 1 1  
    -------  
    0 1 1 1

结果:0111(十进制7)

1.3 按位异或 ^

  • 计算规则: 对于两个数的每一位,如果两个相应的位不同(一个为 1,另一个为 0),则结果的该位为 1;如果两个相应的位相同(都为 0 或都为 1),则结果的该位为 0。(遵循 "相同为 0,不同为 1" 规则)。
  • 示例: 5 ^ 3
    • 5 的二进制表示:0101
    • 3 的二进制表示:0011
    • 按位异或操作过程:
bash 复制代码
    0 1 0 1  
^  
    0 0 1 1  
    -------  
    0 1 1 0

结果:0110(十进制6)

为了清晰展示 8 位二进制数的位运算过程,我们可以使用 char 类型来进行按位与、按位或和异或操作的演示。由于 char 类型通常占用 8 位,非常适合作为位运算的示例载体。程序如下:

cpp 复制代码
#include <stdio.h>

int main()
{
    // 使用 char 类型(通常是 8 位)
    char a = 17;  // 二进制表示为 0001 0001
    char b = -12; // -12 的补码表示为 1111 0100

    // 按位与操作 &
    // 计算规则:【有 0 为 0,全 1 为 1】
    //   0001 0001  ->  17
    // &
    //   1111 0100  ->  -12
    //   0001 0000  ->  16
    printf("a & b = %d\n", a & b); // 输出为 16

    // 按位或操作 |
    // 计算规则:【有 1 为 1,全 0 为 0】
    //   0001 0001  ->  17
    // |
    //   1111 0100  ->  -12
    //   1111 0101  ->  -11
    printf("a | b = %d\n", a | b); // 输出为 -11

    // 按位异或操作 ^
    // 计算规则:【相同为 0,不同为 1】
    //   0001 0001  ->  17
    // ^
    //   1111 0100  ->  -12
    //   1110 0101  ->  -27
    printf("a ^ b = %d\n", a ^ b); // 输出为 -27

    return 0;
}

程序计算过程分析:

程序在 VS Code 中的运行结果如下所示:

1.4 按位取反 ~

  • 计算规则: 对一个数的二进制表示中的每一位(包括符号位)进行取反操作,即将所有的 0 变为 1,所有的 1 变为 0。
  • 示例: 例如,对于 char 类型(8 位)整数 17
    • 其二进制表示为 0001 0001
    • 按位取反后变为 1110 1110
    • 结果为 -18
cpp 复制代码
#include <stdio.h>

int main()
{
    // 使用 char 类型(通常是 8 位)
    char a = 17;  // 二进制表示为 0001 0001
    char b = -12; // -12 的补码表示为 1111 0100

    // 按位取反操作 ~a
    //   0001 0001  ->  17
    // ~
    //   1110 1110  ->  -18
    printf("~a = %d\n", ~a); // 输出为 -18

    // 按位非操作 ~b
    //   1111 0100  ->  -12
    // ~
    //   0000 1011  ->  11
    printf("~b = %d\n", ~b); // 输出为 11

    return 0;
}

程序计算过程分析:

程序在 VS Code 中的运行结果如下所示:

1.5 按位左移 <<

  • 功能: 将一个数的二进制表示向左移动指定的位数。左移时,左侧边缘超出的位将被丢弃,而在右侧边缘新增的位将用 0 填充。

  • 语法:a << b

    • a 是要被左移的数。
    • b 是指定左移的位数。
  • 计算规则:

    • **移动操作:**将 a 的二进制表示(补码)向左移动 b 位。
    • 填充规则:
      • 左侧边缘超出的位将被丢弃。
      • 在右侧边缘新增的位用 0 填充。
  • 注意事项:

    • 未定义行为: 如果指定左移的位数 b 是负数,则行为是未定义的(Undefined Behavior, UB)。
    • 整数溢出: 左移后的值可能超出该整数类型的表示范围,导致整数溢出。
    • 数学应用:左移操作通常用于将数乘以 2 的幂次方(假设没有溢出)
    • 无符号整数: 对于无符号整数,左移后的结果将保持为无符号数。
    • 有符号整数: 对于有符号整数,左移可能导致符号位的变化,进而影响整数的正负。例如,一个正数左移后符号位变为 1,那么结果将是一个负数。
  • **示例:**5 << 1

    • 5 的八位二进制表示为:0000 0101
    • 左移 1 位后为 0000 1010,即十进制数 10
    • 左移 1 位相当于乘以 2 的 1 次方,即 5 * 2^1 = 10
cpp 复制代码
#include <stdio.h>

int main()
{
    // 演示未定义行为
    int a = 17;
    int undefined_shift = -1;
    int result = a << undefined_shift;
    printf("负数移位是未定义行为,示例: %d << %d = %d \n", a, undefined_shift, result); // 输出可能是任意值

    // 演示数学应用
    int b = 5;
    // 左移操作通常用于将数乘以 2 的幂次方(假设没有溢出)
    printf("%d << 4 = %d (5 * 2^4 = 80)\n", b, b << 4); // 输出为 80

    // 演示无符号整数
    unsigned int e = 17;
    printf("无符号整数示例: %u << 2 = %u\n", e, e << 2); // 输出为 68

    // 演示有符号整数
    int f = -12;
    printf("有符号整数示例: %d << 2 = %d\n", f, f << 2); // 输出为 -48

    return 0;
}

程序计算过程分析:

程序在 VS Code 中的运行结果如下所示:

1.6 按位右移 >>

  • 功能: 将一个数的二进制表示向右移动指定的位数。右移时,右侧边缘超出的位将被丢弃,而左侧边缘新增的位根据整数的类型(有符号还是无符号)有不同的填充规则。
  • 语法:a >> b
    • a 是要被右移的数。
    • b 是指定右移的位数。
  • 计算规则:
    • **移动操作:**将 a 的二进制表示(补码)向右移动 b 位。
    • 填充规则:
      • 右侧边缘超出的位将被丢弃。
      • 无符号整数: 表现为逻辑右移,即左侧用 0 填充。
      • 有符号整数: 在左侧边缘新增的位的填充规则依赖于编译器的实现,通常是算术右移,即用符号位填充,但这不是标准强制要求的。
  • 算术右移 (Arithmetic Right Shift):
    • 功能: 在右移时,左侧空出的位用符号位填充。
    • 效果: 保持数值的符号不变。
    • 适用: 用于有符号整数类型(如 int、char 等)。
    • 示例: -10 >> 1
      • -10 的二进制表示为 1111 0110(假设 8 位系统)
      • 算术右移 1 位后为 1111 1011,即 -5
  • 逻辑右移 (Logical Right Shift):
    • 功能: 在右移时,左侧空出的位用 0 填充。
    • 效果: 不保持数值的符号,适用于无符号数。
    • 适用: 用于无符号整数类型(如 unsigned int、unsigned char 等)。
    • 示例: 10 >> 1
      • 10 的二进制表示为 0000 1010
      • 逻辑右移 1 位后为 0000 0101,即 5
  • 注意事项:
    • 未定义行为: 如果指定右移的位数 b 是负数,则行为是未定义的(Undefined Behavior, UB)。
    • 数学应用:对于无符号整数,右移操作通常用于将数除以 2 的幂次方(结果总是向下取整)
    • 实现依赖: 对于有符号整数,C 标准没有规定必须使用算术右移还是逻辑右移,这取决于编译器的具体实现。
cpp 复制代码
#include <stdio.h>

int main()
{
    // 演示未定义行为
    int a = 256;
    int undefined_shift = -2;
    int result = a >> undefined_shift;
    printf("负数移位是未定义行为,示例: %d >> %d = %d \n", a, undefined_shift, result); // 输出可能是任意值

    // 演示无符号整数的逻辑右移
    unsigned int b = 17;
    printf("无符号整数示例: %u >> 3 = %u\n", b, b >> 3); // 输出为 2

    // 演示有符号整数的算术右移
    int c = -12;
    printf("有符号整数示例: %d >> 3 = %d\n", c, c >> 3); // 输出为 -2

    // 演示算术右移和逻辑右移的区别
    unsigned int d = 128;
    printf("无符号整数(逻辑右移)示例: %u >> 1 = %u\n", d, d >> 1); // 输出为 64

    int e = -128;
    printf("有符号整数(算术右移)示例: %d >> 1 = %d\n", e, e >> 1); // 输出为 -64

    // 演示数学应用
    // 对于无符号整数,右移操作通常用于将数除以 2 的幂次方(结果总是向下取整)
    int f = 85;
    printf("%d >> 3 = %d (85 / 2^3 = 10)\n", f, f >> 3); // 输出为 10 (85/8 = 10.625,向下取整为 10)
    // 对于有符号整数,右移操作的行为依赖于编译器的实现
    // 需看编译器使用算术右移还是逻辑右移
    int g = -80;
    printf("%d >> 3 = %d (-80 / 2^3 = -10)\n", g, g >> 3); // 输出为 -10 (-80/8 = -10)

    return 0;
}

程序计算过程分析:

程序在 VS Code 中的运行结果如下所示:

1.7 位运算符的意义与应用

在 C 语言中,位运算符(包括按位与 &、按位或 |、按位异或 ^、按位非 ~、左移 << 和右移 >>)提供了对整数类型底层二进制位的直接操作能力。这类操作在系统级或嵌入式编程中尤为重要,特别是在需要精确控制硬件寄存器、标志位或其他底层资源的场景中。

位运算的核心应用之一是对特定比特位的置位(设为 1)和复位(设为 0),而不会影响其他位的状态。例如:

  • 将第 n 位置为 1:使用按位或操作结合掩码 (1 << n),即执行 x |= (1 << n) ;

    • (1 << n)****:

      • 这是创建一个掩码(mask),它的作用是生成一个只有第 n 位为 1,其余都为 0 的数。例如:

        • 1 << 0 → 0b00000001 (第 0 位为 1)

        • 1 << 1 → 0b00000010 (第 1 位为 1)

        • 1 << 3 → 0b00001000 (第 3 位为 1)

      • 注意:通常从右往左数位,最低位是第 0 位。

    • x | (1 << n):

      • 用原来的值 x 和这个掩码做按位或操作。

      • 因为 OR 的规则是:只要有 1 就为 1。

      • 所以无论原来第 n 位是 0 还是 1,经过 OR 后都会变成 1。

  • 将第 n 位置为 0:使用按位与操作结合取反后的掩码 ~(1 << n),即执行 x &= ~(1 << n) ;

    • (1 << n):

      • 同上,先生成一个只有第 n 位为 1 的掩码。
    • ~(1 << n):

      • 对这个掩码取反。原来是 0b00001000,取反后变成 0b11110111。

      • 这样我们就得到了一个除了第 n 位是 0,其他都是 1 的掩码。

    • x & ~(1 << n):

      • 将 x 和这个新掩码进行按位与操作。

      • AND 的规则是:只要有一个是 0,结果就是 0。

      • 所以:

        • 第 n 位一定是 0(因为它和掩码中的 0 做 AND)

        • 其他位保持原样(因为掩码中是 1,所以保留原来的值)

这些操作允许程序员高效地修改寄存器中的某些位,从而实现对硬件状态(如高低电平控制)的精细管理。由于位运算直接作用于数据的二进制表示,它们通常具有较高的执行效率,适合对性能敏感或资源受限的环境。


2 赋值运算符

在 C 语言中,赋值运算符用于将一个值赋予变量或表达式,它们是编程中非常基础且重要的操作。以下是 C 语言中常见的赋值运算符及其用法。

| 运算符 | 描述 | 操作数个数 | 表达式的值 | 副作用 |
| = | 赋值 | 2 | 左边操作数的值(赋值后的值) | 有,左边操作数的值被更新 |
| += | 相加赋值 | 2 | 左边操作数的值(相加后的值) | 有,左边操作数的值被更新 |
| -= | 相减赋值 | 2 | 左边操作数的值(相减后的值) | 有,左边操作数的值被更新 |
| *= | 相乘赋值 | 2 | 左边操作数的值(相乘后的值) | 有,左边操作数的值被更新 |
| /= | 相除赋值 | 2 | 左边操作数的值(相除后的值) | 有,左边操作数的值被更新 |
| %= | 取余赋值 | 2 | 左边操作数的值(取余后的值) | 有,左边操作数的值被更新 |
| <<= | 左移赋值 | 2 | 左边操作数的值(左移后的值) | 有,左边操作数的值被更新 |
| >>= | 右移赋值 | 2 | 左边操作数的值(右移后的值) | 有,左边操作数的值被更新 |
| &= | 按位与赋值 | 2 | 左边操作数的值(按位与后的值) | 有,左边操作数的值被更新 |
| ^= | 按位异或赋值 | 2 | 左边操作数的值(按位异或后的值) | 有,左边操作数的值被更新 |

|= 按位或赋值 2 左边操作数的值(按位或后的值) 有,左边操作数的值被更新

2.1 左值和右值

  • 左值(Lvalue):左值是一个具有确定内存位置的变量或表达式,可以出现在赋值操作的左侧。例如:变量名、数组元素、结构体成员等。
  • 右值(Rvalue):右值是一个表示值的表达式,但没有明确的内存位置。右值通常是一个常量、算术表达式的结果、函数调用返回的结果等。

2.2 注意事项

  • 赋值运算符的第一个操作数(左值)必须是变量的形式,第二个操作数可以是任何形式的表达式。

  • 左值必须可修改: 赋值操作要求左侧必须是一个左值,即一个可以存储新值的内存位置

  • **右值提供值:**赋值操作的右侧可以是一个右值,它提供了要赋给左值的具体值。

  • 示例:

    • 正确用法:a = b + 25;(a 是左值,b + 25 是右值)
    • 错误用法:b + 25 = a;(尝试将右值 b + 25 用作左值,这是不允许的)
    • 编译错误结果,如下图所示:

2.3 复合赋值运算符的使用

复合赋值运算符(如 +=、-= 等)的用法是将等号右边的表达式首先计算为一个整体的值,然后将这个值与等号左边的变量进行相应的运算,并将结果赋值回该变量。

cpp 复制代码
a += b + 2; 
等价于
a = a + (b + 2);

2.4 案例演示

cpp 复制代码
#include <stdio.h>

int main()
{
    int a = 10, b = 20, c = 30, d = 4;

    // 简单的赋值
    a = 5;
    printf("a = %d\n", a); // a = 5

    // 加法赋值
    c += 3;                // c = c + 3 = 30 + 3
    printf("c = %d\n", c); // c = 33

    // 减法赋值
    c -= b;                // c = c - b = 33 - 20
    printf("c = %d\n", c); // c = 13

    // 乘法赋值
    a *= 2;                // a = a * 2 = 5 * 2
    printf("a = %d\n", a); // a = 10

    // 除法赋值
    b /= 2;                // b = b / 2 = 20 / 2
    printf("b = %d\n", b); // b = 10

    // 取模赋值
    c %= 3;                // c = c % 3 = 13 % 3
    printf("c = %d\n", c); // c = 1

    // 连等写法
    int e = 12, f;
    f = e *= a; // f = e * a = 12 * 10
    // 先计算 e *= a,然后将结果赋值给 f
    printf("e = %d\n", e); // e = 120
    printf("f = %d\n", f); // f = 120

    // 左移赋值
    d <<= 2;               // d = d << 2 = 4 << 2
    printf("d = %d\n", d); // d = 16

    // 右移赋值
    d >>= 1;               // d = d >> 1 = 16 >> 1
    printf("d = %d\n", d); // d = 8

    // 按位与赋值
    a &= 1;                // a = a & 1 = 10 & 1
    printf("a = %d\n", a); // a = 0

    // 按位异或赋值
    b ^= 3;                // b = b ^ 3 = 10 ^ 3
    printf("b = %d\n", b); // b = 9

    // 按位或赋值
    b |= 4;                // b = b | 4 = 9 | 4
    printf("b = %d\n", b); // b = 13

    return 0;
}

程序在 VS Code 中的运行结果如下所示:

2.5 复合赋值运算符表达式的值

在 C 语言中,复合赋值运算符(如 +=、-=、*= 等)不仅简化了赋值操作,还具有特定的表达式值。复合赋值运算符组成的表达式,其值为赋值后的左边操作数的值。以下通过一个案例来演示这一特性:

cpp 复制代码
#include <stdio.h>

int main()
{
    int n = 5;

    // 使用复合赋值运算符 +=,并打印表达式的结果
    printf("n += 3 的结果是: %d\n", n += 3);
    // 执行过程:
    // 1. 计算 n + 3 = 8
    // 2. 将 8 赋给 n
    // 3. 表达式 n += 3 的值为赋值后的 n 的值,即 8
    // 输出:n += 3 的结果是: 8

    // 验证 n 的值
    printf("此时 n 的值是: %d\n", n); // 输出:此时 n 的值是: 8

    return 0;
}
  • 初始时,n 的值为 5。
  • 执行 n += 3 时,先计算 n + 3 的值(即 8),然后将该值赋给 n。
  • 表达式 n += 3 的值为赋值后的 n 的值,即 8,因此 printf 函数会输出 8。
  • 最后,再次打印 n 的值,确认 n 已经被更新为 8。

程序在 VS Code 中的运行结果如下所示:

2.6 连等写法

C 语言支持连等写法,即在一个表达式中连续进行多个赋值操作。连等写法的赋值顺序是从右向左进行的。以下通过一个案例来演示连等写法:

cpp 复制代码
#include <stdio.h>

int main()
{
    int x, y;

    // 使用连等写法 x = y = 99
    x = y = 99;

    // 执行过程解析:
    // 1. y = 99; 先执行,将 y 赋值为 99
    // 2. 然后 x = y; 执行,将 x 赋值为 y 的值,即 99

    // 打印 x 和 y 的值
    printf("x = %d, y = %d\n", x, y); // 输出:x = 99, y = 99

    return 0;
}
  • 在 x = y = 99; 这个连等写法中,赋值操作是从右向左进行的。
  • 首先,99 被赋给 y,此时 y 的值为 99。
  • 然后,y 的值(即 99)被赋给 x,此时 x 的值也为 99。
  • 最终,printf 函数输出 x = 99, y = 99,验证了连等写法的执行顺序和赋值过程。

程序在 VS Code 中的运行结果如下所示:


3 三元运算符

3.1 基本语法

三元运算符(也称为条件运算符)的基本语法如下:

cpp 复制代码
条件表达式 ? 表达式1 : 表达式2;
  • **条件表达式:**一个逻辑表达式,用于判断其真假。
  • **表达式1:**如果条件表达式为真(非 0),则整个三元运算符的值为表达式 1 的值。
  • **表达式2:**如果条件表达式为假(0),则整个三元运算符的值为表达式 2 的值。

3.2 案例演示

cpp 复制代码
#include <stdio.h>

int main()
{
    int a = 99;
    int b = 99;

    // 使用三元运算符来决定 res 的值
    int res = a > b ? a++ : b--;
    // 因为 a 不大于 b,所以执行 b--,并将结果赋值给 res
    // 注意:b-- 是后缀自减,意味着先返回 b 的当前值(99),然后再将 b 减 1
    // res 被赋值为 99,然后 b 变为 98
    printf("a=%d\n", a);     // 输出 a=99,因为 a 没有被改变
    printf("b=%d\n", b);     // 输出 b=98,因为 b 在前面的条件运算符中自减了
    printf("res=%d\n", res); // 输出 res=99,因为 res 被赋值为 b 自减之前的值

    float n1 = a > b ? 1.1 : 1.2; // 条件表达式为真,整个表达式的值是表达式1:1.1
    // 注意:由于 b 在前面的条件运算符中已经被自减,所以这里 b 的值是 98
    printf("n1=%.2f\n", n1); // 输出 n1=1.10

    return 0;
}

程序在 VS Code 中的运行结果如下所示:

3.3 案例:计算两个数的最大值

cpp 复制代码
#include <stdio.h>

int main()
{
    int a = 10;
    int b = 100;

    // 使用三元运算符计算 a 和 b 中的最大值
    int max = a > b ? a : b;

    printf("a 和 b 中最大的数字:%d", max); // 输出:a 和 b 中最大的数字:100

    return 0;
}

程序在 VS Code 中的运行结果如下所示:

3.4 案例:计算三个数的最大值

cpp 复制代码
#include <stdio.h>

int main()
{
    int a = 10;
    int b = 100;
    int c = 199;

    // 1. 使用两次三元运算符来计算 a、b 和 c 中的最大值
    int max_a_b = a > b ? a : b;
    int max_a_b_c = max_a_b > c ? max_a_b : c;
    // 输出 a、b 和 c 中最大的数字
    printf("a、b、c 中最大的数字:%d\n", max_a_b_c); // 输出:a、b、c 中最大的数字:199

    // 2. 使用嵌套的三元运算符来找出 a、b、c 中的最大值
    // 首先比较 a 和 b,然后将较大的值与 c 比较
    int max = (a > b ? a : b) > c ? (a > b ? a : b) : c;
    // 输出 a、b 和 c 中最大的数字
    printf("a、b、c 中最大的数字:%d\n", max); // 输出:a、b、c 中最大的数字:199

    // 3. 也可以简化为下面的写法:
    max = (a > b ? (a > c ? a : c) : (b > c ? b : c));
    // 输出 a、b 和 c 中最大的数字
    printf("a、b、c 中最大的数字:%d\n", max); // 输出:a、b、c 中最大的数字:199

    // 4. 也可以使用 if-else 语句来找出 a、b、c 中的最大值
    // 后续在学习 if-else 语句时会详细讲解

    return 0;
}

程序在 VS Code 中的运行结果如下所示:


4 逗号运算符

4.1 基本语法

逗号运算符(,)是 C 语言中一个简单但强大的运算符,用于将多个表达式连接在一起。它的基本语法如下:

cpp 复制代码
表达式1, 表达式2, 表达式3, ..., 表达式N;
  • **表达式1, 表达式2, ..., 表达式N:**这些是逗号运算符连接的多个表达式。

4.2 表达式的值

逗号运算符的特性如下:

  • 求值顺序: 逗号运算符从左到右依次计算每个表达式的值。
  • 表达式的值: 整个逗号表达式的值为最后一个表达式的值。

4.3 案例演示

cpp 复制代码
#include <stdio.h>

int main()
{
    int a = 5, b = 10, c = 15;

    // 使用逗号运算符依次执行多个表达式
    int result = (a = b, b = c, c = a + b);
    // 执行过程解析:
    // 1. a = b,将 b 的值赋给 a,此时 a = 10, b = 10, c = 15
    // 2. b = c,将 c 的值赋给 b,此时 a = 10, b = 15, c = 15
    // 3. c = a + b,将 a 和 b 的和赋给 c,此时 a = 10, b = 15, c = 25
    // 最后,result 被赋值为 c 的值,即 25

    // 打印结果
    printf("a = %d, b = %d, c = %d\n", a, b, c); // 输出:a = 10, b = 15, c = 25
    printf("result = %d\n", result);             // 输出:result = 25

    return 0;
}

程序在 VS Code 中的运行结果如下所示:

4.4 使用场景

逗号运算符通常用于以下场景:

  • **简化代码:**在需要执行多个操作但不想使用多行代码时,可以使用逗号运算符。
  • 循环和条件语句: 在 for 循环的初始化或更新部分,或者在需要多个条件判断的地方,可以使用逗号运算符。

4.5 注意事项

  • **可读性:**虽然逗号运算符可以简化代码,但过度使用可能会降低代码的可读性。
  • **优先级:**逗号运算符的优先级较低,因此在复杂的表达式中,建议使用括号来明确运算顺序。

5 运算符优先级

5.1 运算符优先级表格说明

优先级 运算符 名称或含义 结合方向
1 [] 数组下标 左到右
() 函数调用、圆括号
. 成员选择(对象)
-> 成员选择(指针)
2 -(单目) 负号运算符 右到左
(类型) 强制类型转换
++ 自增运算符
-- 自减运算符
*(单目) 取值运算符(解引用)
&(单目) 取地址运算符
! 逻辑非运算符
~ 按位取反运算符
sizeof 长度运算符
3 / 左到右
*(双目)
% 余数(取模)
4 + 左到右
-(双目)
5 << 左移 左到右
>> 右移
6 > 大于 左到右
>= 大于等于
< 小于
<= 小于等于
7 == 等于 左到右
!= 不等于
8 & 按位与 左到右
9 ^ 按位异或 左到右
10 | 按位或 左到右
11 && 逻辑与 左到右
12 || 逻辑或 左到右
13 ?: 条件运算符 右到左
14 = 赋值运算符 右到左
/= 除后赋值
*= 乘后赋值
%= 取模后赋值
+= 加后赋值
-= 减后赋值
<<= 左移后赋值
>>= 右移后赋值
&= 按位与后赋值
^= 按位异或后赋值
|= 按位或后赋
15 , 逗号运算符 左到右
  • 优先级记忆: 虽然运算符优先级表看起来复杂,但通常不需要刻意记忆所有优先级。一般来说,一元运算符(如 !、-(单目)、++、-- 等)的优先级高于算术运算符,算术运算符的优先级高于关系运算符,关系运算符的优先级高于逻辑运算符,逻辑运算符的优先级高于三元运算符,三元运算符的优先级高于赋值运算符。
  • 常用优先级关系:自增自减 > 逻 辑非 ! > 算术运算符 > 关系运算符 > 逻辑与 && > 逻辑或 || > 条件运算符 ?: > 赋值运算符
  • 使用括号: 如果对运算符的优先级不确定,或者为了代码的可读性,可以直接使用括号来明确运算顺序。括号可以覆盖任何运算符的优先级,确保表达式按照预期的方式计算。

5.2 复杂表达式的计算分析

表达式 5 > 3 && 8 < 4 - !0 的计算过程分析

**问题:**对于表达式 5 > 3 && 8 < 4 - !0 的最终值是多少?计算过程是怎样的?

计算过程:

  1. **短路性质:**首先计算 && 左边的表达式 5 > 3,其逻辑值为 1。
  2. 右边表达式: 计算 && 右边的表达式 8 < 4 - !0。
    • 非运算:!0 的逻辑值为 1。
    • **算术运算:**4 - 1 的结果为 3。
    • **关系运算:**8 < 3 的逻辑值为 0。
  3. **逻辑与运算:**整个表达式的值为 1 && 0,最终逻辑值为 0。

表达式 a + b < c && b == c && a || b + c && b + c 的计算过程分析

**问题:**若 a = 2,b = 3,c = 4,表达式 a + b < c && b == c && a || b + c && b + c 的计算过程是怎样的?值为多少?

计算过程:

  1. **短路性质:**首先计算 a + b < c,即 2 + 3 < 4,其逻辑值为 0。
  2. **短路效果:**b == c && a 不会执行,|| 左边的值为 0。
  3. 右边表达式: 计算 b + c && b + c。
    • **算术运算:**b + c 的结果为 7。
    • **逻辑与运算:**7 && 7 的逻辑值为 1。
  4. **逻辑或运算:**0 || 1 的逻辑值为 1。

表达式 (m = a > b) && (n = c > d) 的计算过程分析

**问题:**设有 int a = 1, b = 2, c = 3, d = 4, m = 2, n = 2;执行 (m = a > b) && (n = c > d) 后 m 和 n 的值分别是多少?表达式的结果是多少?

计算过程:

  1. 短路性质: 首先计算(m = a > b)。
    • **关系比较:**a > b,即 1 > 2,其逻辑值为 0。
    • **赋值运算:**m = 0。
  2. 短路效果:(n = c > d) 不执行。
  3. 最终结果:
    • m 的值为 0。
    • n 的值为 2。
    • 整个表达式的结果为 0。

总结与建议:

  1. **避免复杂表达式:**尽量简化表达式。
  2. **明确执行顺序:**使用小括号明确执行顺序,避免依赖运算符优先级。
  3. **拆分表达式:**复杂表达式拆分为多个步骤,提高可读性和可维护性。

6 编程练习

6.1 会员折扣

某商店对会员提供折扣:

  • 消费满 100 元,打 9 折
  • 消费满 200 元,打 8 折
  • 否则,无折扣

编写一个程序,根据消费金额计算最终价格。

cpp 复制代码
#include <stdio.h>

int main()
{
    float amount; // 消费金额
    printf("请输入消费金额:");
    scanf("%f", &amount); // 输入消费金额

    float final_amount = (amount >= 200) ? amount * 0.8 : (amount >= 100) ? amount * 0.9
                                                                          : amount;

    printf("最终价格:%.2f 元\n", final_amount);

    return 0;
}

程序在 VS Code 中的运行结果如下所示:

6.2 多任务处理

假设你在管理一个任务列表,每个任务有 ID、优先级和状态。编写一个程序,更新任务的状态和优先级,并输出更新后的信息。

cpp 复制代码
#include <stdio.h>

int main()
{
    int task_id = 101, priority = 3, status = 0;

    // 更新任务优先级和状态
    int updated = (priority = 5, status = 1);

    printf("任务 ID:%d,优先级:%d,状态:%d\n", task_id, priority, status);

    return 0;
}

程序在 VS Code 中的运行结果如下所示:

6.3 灯的开关状态

你有一个 8 位的整数表示 8 个灯的状态(1 表示开,0 表示关)。初始状态为所有灯关闭。编写一个程序,打开第 2 和第 7 个灯,并关闭第 4 个灯(如果它是开的)。

cpp 复制代码
#include <stdio.h>

int main()
{
    unsigned char lights = 0b00000000; // 所有灯关

    // 打开第 2 和第 7 个灯
    lights |= (1 << 1) | (1 << 6);

    // 关闭第 4 个灯(如果它是开的)
    lights &= ~(1 << 3);

    return 0;
}

6.4 权限检查

在一个系统中,权限使用一个整数的二进制位表示。例如,0000 1010 表示用户有权限 2 和 4(从右到左,最低位为权限 1)。编写一个程序,给用户添加权限 3,并检查用户是否有权限 4。

cpp 复制代码
#include <stdio.h>

int main()
{
    unsigned char permissions = 0b00001000; // 初始权限

    // 添加权限 3
    permissions |= (1 << 2);

    // 检查是否有权限 4
    int has_permission_4 = (permissions & (1 << 3)) != 0;
    // 解释上面这行代码
    // (permissions & (1 << 3)) != 0
    // permissions & (1 << 3) 是将 permissions 的第 3 位与 1 进行按位与操作,如果第 3 位为 1,则结果为 1,否则为 0。
    // != 0 是判断结果是否不等于 0,如果不等于 0,则表示第 3 位为 1,即有权限 4,否则没有权限 4。
    // 加上 != 0 使得代码更加规范,因为 (permissions & (1 << 3)) 的结果是一个整数,而不是一个布尔值。

    printf("用户是否有权限 4?%s\n", has_permission_4 ? "有" : "无");

    return 0;
}

程序在 VS Code 中的运行结果如下所示:

6.5 温度更新

假设你有一个初始温度值为 25 摄氏度。由于天气变化,温度变化如下:

  1. 上午升温 3 度。
  2. 下午降温 2 度。
  3. 晚上升温 1 度。

编写一个程序,计算晚上的最终温度。

cpp 复制代码
#include <stdio.h>

int main()
{
    float temperature = 25;

    temperature += 3; // 上午升温
    temperature -= 2; // 下午降温
    temperature += 1; // 晚上升温

    printf("晚上的最终温度:%.2f 摄氏度\n", temperature);

    return 0;
}

程序在 VS Code 中的运行结果如下所示:

相关推荐
Dream it possible!8 天前
LeetCode 热题 100_只出现一次的数字(96_136_简单_C++)(哈希表;哈希集合;排序+遍历;位运算)
c++·leetcode·位运算·哈希表·哈希集合
ErizJ21 天前
Golang | 位运算
开发语言·后端·golang·位运算
吗喽对你问好22 天前
Java位运算符大全
java·开发语言·位运算
小卡皮巴拉23 天前
【力扣刷题实战】丢失的数字
c++·算法·leetcode·位运算
ゞ 正在缓冲99%…24 天前
leetcode201.数字范围按位与
java·数据结构·算法·位运算
清羽_ls1 个月前
leetcode-位运算
前端·算法·leetcode·位运算
daily_23332 个月前
coding ability 展开第九幕(位运算——进阶篇)超详细!!!!
算法·位运算
_唐浮2 个月前
【HarmonyOS Next】常见的字节转换
harmonyos·arkts·位运算·16进制
天天超方的2 个月前
【题解】CF2077B Finding OR Sum
位运算·交互题·cf