位掩码(bit masking)就是把数据真正按位来存,不用 int、char 各自占一大块,而是把一个整数的各个二进制位当成一个个开关(flag)。省内存又高效,适合"开/关""动/静"这种只有两种状态的信息。

基础:布尔逻辑与位

位掩码的思想建立在布尔逻辑上,核心四件事:

运算规则
NOT(非)1→0,0→1
AND(与)两个都是 1 结果才为 1
OR(或)任一个是 1 结果就为 1
XOR(异或)一个 1 一个 0 结果才为 1

一个布尔值就是一个位(bit)。C 语言的 intchar 由若干位组成,8 位是一字节。标准保证 char 至少 1 字节、int 至少 16 位。C11 的 stdint.h 还允许指定精确字节数的类型,做掩码特别方便。

0b 0000 0101 表示 FLAG_ALIVE(bit2) 和 FLAG_ON(bit0) 都开着 0 bit7 0 bit6 0 bit5 0 bit4 0 bit3 1 bit2 0 bit1 1 bit0 第 n 位就是一个 flag:1 表示开、0 表示关
一个 int 的各二进制位当开关用:置位/清零/翻转只动某一位,比每个状态各存一个 int 省内存。

四个基本操作

假设要操作第 n 位,掩码就是 1 << n(把 1 推到第 n 位)。四个招式:

/* 1. 置位:把第 n 位设成 1 */
storage |= (1 << n);

/* 2. 清零:把第 n 位设成 0 */
storage &= ~(1 << n);

/* 3. 翻转:第 n 位 1↔0 */
storage ^= (1 << n);

/* 4. 检查:第 n 位是 1 吗 */
bit = storage & (1 << n);   /* 非 0 即该位为 1 */

置位(Set bit n)

用 OR 把目标位或成 1。例:给 storage 置第 3 位:

01000010   (当前 storage)
   OR
00001000   (1 << 3,即 2^3)
   ==
01001010

1 << n 这步把"1"放到掩码的正确位置,OR 之后只碰那一位,别的位原封不动。

清零(Clearing bit n)

用 AND 上掩码的取反:目标位被清零,其余位保持。接着上面的结果清第 3 位:

01001010
   AND
11110111   (~(1 << 3))
   ==
01000010

翻转(Flipping bit n)

用 XOR:该位原来是 0 变 1、是 1 变 0,其余位不变:

01000010    01001010
   XOR         XOR
00001000    00001000
   ==          ==
01001010    01000010

检查(Checking bit n)

用 AND 上掩码,结果非 0 就是"该位为 1":

01000010    01001010
   AND         AND
00001000    00001000
   ==          ==
00000000    00001000   ← 后者说明第 3 位是 1

实战:一组游戏状态标记

把一堆开关装进一个 short。先定义各个位的常量:

const short FLAG_ON          = 1 << 0;  // 1  (0x01)
const short FLAG_MOVEMENT    = 1 << 1;  // 2  (0x02)
const short FLAG_TRANSPARENT = 1 << 2;  // 4  (0x04)
const short FLAG_ALIVE       = 1 << 3;  // 8  (0x08)
const short FLAG_BROKEN      = 1 << 4;  // 16 (0x10)
const short FLAG_EDIBLE      = 1 << 5;  // 32 (0x20)

int main() {
    short attributes = 0;

    /* 一次打开 ON、TRANSPARENT、BROKEN */
    attributes |= FLAG_ON;
    attributes |= FLAG_TRANSPARENT;
    attributes |= FLAG_BROKEN;

    /* 保留 ON 和 ALIVE,其余全清 */
    attributes &= FLAG_ON | FLAG_ALIVE;

    /* 检查 ALIVE 是不是开着 */
    if (attributes & FLAG_ALIVE) {
        /* ... */
    }
    /* 检查 BROKEN 是不是关着 */
    if (!(attributes & FLAG_BROKEN)) {
        /* ... */
    }
    return 0;
}

一个 short 就能同时记 16 个开关,而 16 个 bool 要占 16 字节,位掩码的省法就是这么来的。

什么时候用位掩码

  • 权限系统:读、写、执行三个位的组合。
  • 硬件寄存器:状态/命令按位解读。
  • 大量布尔状态要省内存。

练手题:给上面那组 flag 定义好 FLAG_ALIVE、FLAG_BROKEN,然后用"置位/清零/翻转"把状态改成恰好是 ON + ALIVE,再用位运算断言检查 ALIVE 开着、BROKEN 关着。