Skip to content

位操作

1. 位与二进制

1.1 计算机中的位

  1. 定义:
    • 位是 最小的 计算机 数据存储 单位
    • 每一个位存储一个二进制码(记作 01
  2. 对于 整型 数据来说,每个计算机位对应这个整数二进制表示中的一个位
  3. 8 位构成一个 字节,因此 在常见的实现中int 类型有 4 字节(32 位),long long8 字节(64 位)
  4. 关于进制的知识,可以参考 进制

1.2 整数的二进制表示(以一字节为例)

  1. 无符号:即 unsigned char 类型,二进制表示中,所有位都用于表示数值,范围在 0 ~ 255 之间
  2. 有符号:即 signed char 类型:
    • 也许有符号数最简单的表示方法是使用 1 位(最高位)用于表示符号,其余位用于表示数值本身,但这么做有个问题:+0-0 是两个数,这显然是不合理的,因此,在计算机中,有符号数通常使用 二进制补码 来表示(这也是当今最常用的系统)
    • 二进制补码 表示法:最高位设置 0 ,用后 7 位表示 0 ~ 127 。最高位设置 1 ,则表示负数,具体方法如下:
      • 首先,从一个 9 位组合 100000000 减去一个负数的位组合,结果是该负数的量。例如:有符号数 100000000 ,其位组合表示一个无符号数 128 ,因为 100000000 - 10000000 = 10000000 ,即 256 - 128 = 128
      • 再由于其最高位为 1 ,因此,该例的有符号数表示 -128
      • 以此类推:10000001 表示 -12710000010 表示 -126 ,该方法可以表示 -128 ~ +127 的所有整数。

注:逻辑代数中,01 不表示数量的大小,而是表示两种 对立的状态

2. 位操作

只能使用 整型 数据进行位操作,不能使用 **浮点型** 数据;

2.1 位操作的种类

下面的例子使用无符号 8 位整数进行演示:

  1. 按位与 (&):对应位都为 1 时,结果才为 1

    C
    (10011010) & (10110110)  // 表达式
    10010010                 // 结果值

    当然 C 中有一个按位与和赋值结合的运算符:&=

  2. 按位或 (|):对应位只要有一个为 1 或都为 1 ,结果就为 1

    C
    (10011010) | (10110110)  // 表达式
    10111110                 // 结果值

    当然 C 中也有一个按位或和赋值结合的运算符:|=

  3. 按位异或 (^):对应位不同时,结果为 1

    C
    (10011010) ^ (10110110)  // 表达式
    00101100                 // 结果值

    当然 C 中也有一个按位异或和赋值结合的运算符:^=

  4. 按位取反 (~):1001

    C
    ~(10011010)  // 表达式
    01100101     // 结果值
  5. 按位左移 (<<):

    C
    (10011010) << 2  // 表达式
    01101000         // 结果值

    当然 C 中也有一个按位左移和赋值结合的运算符:<<=

  6. 按位右移 (>>):

    C
    (10011010) >> 2  // 表达式,有符号
    00100110         // 在某些实现中的结果
    11100110         // 在另一些实现中的结果

    在不同实现中,对有符号数的右移操作,其左端填补数可以为 1 也可以为 0。大多数实现中,左端填补数和原符号位相同,确保正负不变。

    C
    (10011010) >> 2  // 表达式,无符号
    00100110         // 结果值

    当然 C 中也有一个按位右移和赋值结合的运算符:>>=

2.2 位操作的应用

注:在之后的实际项目里,尤其是对 硬件 开发上,位操作会经常用到 在下面的示例中,使用无符号整数来演示,即unsigned int 类型,默认你们已经引入 stdint.h 头文件:

C
#include <stdint.h>

或者,也可以使用 typedef 定义一些无符号整数类型,如下:

C
typedef unsigned long long    uint64_t;
typedef unsigned int          uint32_t;
typedef unsigned short int    uint16_t;
typedef unsigned char         uint8_t;

示例中的 二进制字面量 只有 8 位,这其实没什么问题,因为二进制位数是由类型决定的,而不是字面量的位数,因此,**字面量位数 ** 可以比 该字面量类型位数 少,也可以比 该字面量类型位数 多。但需要注意的是,二进制存储是 从右往左 的,因此,如果 字面量位数该字面量类型的位数 多,那么,字面量左边的位会被丢弃,如下:

C
uint32_t flag = 0b1111111100000000000000000000000000000000;
// 40位的字面量,如果 int 为 32 位,那么 flag 只能存储 32 个二进制位

// 可能造成如下结果:
// flag = 0b00000000000000000000000000000000(flag 最左的 8 位被丢弃)

若是字面量位数比该字面量类型的位数少,那么,字面量左边的位会被补零。

  1. 掩码 ( MASK )

    • 定义:掩码是一个 二进制数,用于 屏蔽 另一个二进制数的特定位

    • 可以这样类比:掩码就像一个 滤网 ,只有掩码中的 1 是孔,把flag的对应位置给透出来,其余位置被 0 给堵上

    • 下面举个简单的例子:

      C
       //typedef unsigned char    uint8_t;
      
       uint8_t flag = 0b10101010;
      
       //定义MASK,0号位为1,其余位为0
       uint8_t mask = 0b00000010;
      
       //掩码操作
       flag = flag & mask;  //此时 flag = 0b00000010
  2. 打开位 ( SET )

    • 定义:打开位,就是将特定位设置为 1,同时保持其它位不变

    • 使用 | 运算符和掩码,翻译成人话就是:任何位和 1 组合,结果都是 1;任何位和 0 组合,结果都是该位本身

    • 以上一节的 mask(只有 1 号位为 1)为例:

      C
      uint8_t flag = 0b10101001;
      flag |= mask;  //此时 flag = 0b10101011

      根据 mask 中为 1 的位, flag 中对应位设置为 1 ,其余位不变

  3. 清空位 ( CLEAR )

    • 定义:清空位,就是将特定位设置为 0, 同时保持其它位不变

    • 使用 &~ 运算符与掩码,看代码:

    • 以上一节的 mask(只有 1 号位为 1)为例:

      C
      uint8_t flag = 0b10101011;
      flag &= ~mask;  //此时 flag = 0b10101001

      根据 mask 中为 1 的位, flag 中对应位设置为 0 ,其余位不变

  4. 切换位 ( TOGGLE )

    • 定义:切换位,就是将特定位取反,同时保持其它位不变

    • 使用 ^ 运算符和掩码,即:任何位和 1 组合,结果都是该位取反;任何位和 0 组合,结果都是该位本身

    • 以新的 mask0b11110000)为例:

      C
      uint8_t flag = 0b10101011;
      flag ^= mask;  //此时 flag = 0b01011011

      根据 mask 中为 1 的位, flag 中对应位切换,其余位不变

  5. 读取位 ( READ )

    • 定义:读取位,就是只读取特定位的值

    • 使用 & 运算符和掩码,代码如下:

    • 以上一节的 mask(只有 1 号位为 1)为例:

      C
      uint8_t flag = 0b10101011;
      if ((flag & mask) == mask) {
          printf("1号位为1。");
      }

      根据 mask 中为 1 的位, 在 flag 中只读对应。