Skip to content
返回博客
教程

位运算完全指南:AND、OR、XOR、移位与位掩码实战

位运算实战完整指南:AND / OR / XOR / 移位、补码、位掩码与 feature flag,附 JS、Python、Go、C 代码示例与常见陷阱,立即查看。

17 分钟阅读

位运算完全指南:AND、OR、XOR、移位与位掩码实战

翻开一份旧的 PostgreSQL 迁移脚本,看到 permissions & 0b100;同事上线一个把 32 个布尔值塞进单个整数的 feature flag 系统;Kubernetes 吐出 192.168.1.0/24,你得在代码里提取网络地址。三个场景,一项底层技能:位运算。

应用层开发平时几乎不会主动写 &^,直到突然需要。这份指南覆盖六个位运算符、补码表示、九个值得记住的实战模式,以及各语言的坑(JavaScript 尤其多)。代码用 JS、Python、Go、C 四种语言对照,例子全部可运行。

另开一个标签页打开进制转换器,后面几节会让你输入数字,实时观察位模式的变化。

为什么 2026 年还要学位运算

高级语言没让位运算消失,只是把它藏了起来。你今天可能没意识到自己正在依赖它,但它确实还活着:

  • PostgreSQL 行级安全用一个整数位图存 ACL 权限(SELECTINSERTUPDATEDELETE 等)
  • Linux capabilities 用 40 多个权限位替代了「要么 root 要么普通用户」的老模型,合并权限用 |
  • JWT 算法头把哈希算法编码进一个小字段,库层经常做位级比较
  • Snowflake / ULID / UUIDv7 把时间戳、机器 ID、序列号用左移打包进 64 位或 128 位整数
  • Redis 的 BITCOUNT / BITOP 把位运算原语直接暴露给应用层,用于基数估算和 A/B 分桶
  • 图像处理读 32 位 RGBA 像素时,用 &>> 抽取通道
  • 微信小程序与支付宝开放平台的权限位模型,查询接口权限靠一次 AND 搞定

位运算在 CPU 指令级是 O(1)。把 32 个布尔塞进一个整数省下 31 字节内存,更关键的是,「这 32 个标志位有任意一个被设置吗」只需要一次 != 0 判断。

先打好二进制基础

本文假设你已经会看二进制。如果需要复习,先读进制转换完全指南再回来。

先过一遍词汇表:

  • bit(位):0 或 1
  • nibble(半字节):4 位,对应一个十六进制字符
  • byte(字节):8 位
  • word(字):通常 32 位或 64 位,取决于 CPU

大多数语言的整数是定宽的:8、16、32、64 位。位宽在位运算里特别关键,因为移位会把位挤出边界,而有符号整数的符号位正好坐在最左边。

试一下:打开进制转换器,把十进制 170 输进去,二进制输出应该是 10101010,这个交替模式后面会反复出现。

六个位运算符

主流语言给你的是同样的六个运算符,语法差异很小。&|^~<<>> 在 JavaScript、Python、Go、Rust、C、C++、Java、C# 里都一样。JavaScript 多一个 >>>,无符号右移。

AND(&):位过滤器

只有两个输入位都是 1 时,输出位才是 1。

ABA & B
000
010
100
111

把 AND 想成一道闸门:只有两边都置位的位才能通过。最常见的用途是掩码,保留某些位,把其他位清零。

// 抽取低 4 位(最右的 nibble)
const value = 0b11010110;   // 214
const low4  = value & 0x0F; // 0b00000110 = 6

// 判奇偶
const isOdd = (n) => (n & 1) === 1;
isOdd(7);  // true
isOdd(42); // false
# Python 写法一致
value = 0b11010110
low4 = value & 0x0F  # 6

def is_odd(n):
    return (n & 1) == 1

OR(|):位设置器

只要任一输入位是 1,输出位就是 1。

ABA | B
000
011
101
111

OR 用来合并标志。若 READ = 1WRITE = 2EXECUTE = 4,那么 READ | WRITE 就是 3,两个权限同时开启。

const READ  = 0b001;
const WRITE = 0b010;
const EXEC  = 0b100;

const rw = READ | WRITE;  // 0b011 = 3
READ, WRITE, EXEC = 0b001, 0b010, 0b100
rw = READ | WRITE  # 3

XOR(^):位翻转器

两位不同则输出 1。

ABA ^ B
000
011
101
110

XOR 有三条代数性质,撑起了计算机科学里一批聪明技巧:

  • a ^ a = 0,任何值和自己 XOR 都变零
  • a ^ 0 = a,和零 XOR 是恒等
  • a ^ b ^ a = b,XOR 是自己的逆

第三条解释了 XOR 为何出现在奇偶校验、流密码,以及那道经典面试题——「在一个所有数都出现两次、唯一一个数只出现一次的数组里找出那个数」。

// 在每个数都出现两次、只有一个数出现一次的数组里找出那个数
const findUnique = (arr) => arr.reduce((a, b) => a ^ b, 0);
findUnique([4, 1, 2, 1, 2]);  // 4
from functools import reduce
from operator import xor
find_unique = lambda arr: reduce(xor, arr, 0)
find_unique([4, 1, 2, 1, 2])  # 4

NOT(~):位反转器

一元 ~ 翻转所有位:0 变 1,1 变 0。

~0b00001111  // -16(JavaScript 强转 32 位有符号)
~5           // -6
~5  # -6
// Go 的一元位非用 ^,注意区别
var x int8 = 5
fmt.Println(^x)  // -6

几乎所有主流语言里 ~5 都是 -6,初学者一看就懵。原因是补码表示,下一节会讲。先记住:在使用补码表示负数的语言里(所有主流语言都用),~x 恒等于 -(x + 1)

左移(<<):二的幂次乘法

x << nx 所有位向左移 n 位,右侧补 0。数学上等价于乘以 2ⁿ。

1 << 0   // 1    (2^0)
1 << 1   // 2    (2^1)
1 << 3   // 8    (2^3)
1 << 10  // 1024 (2^10 = 1 KiB)

// 构造标志位
const FLAG_ADMIN    = 1 << 0;
const FLAG_EDITOR   = 1 << 1;
const FLAG_REVIEWER = 1 << 2;

1 << n 的妙处在于生成一个只在位置 n 上有 1 的数,那一位就是一个标志。

注意溢出。JavaScript 里 1 << 31-2147483648(不是 2147483648),因为 JS 位运算强制走 32 位有符号整数。

右移(>> vs >>>):带符号还是补零?

右移把位向右挪。问题是:左侧空出来的位填什么?

  • >>(算术右移):保留符号位,负数依然为负
  • >>>(逻辑/无符号右移):补 0,JavaScript 独有的专门运算符
-8 >> 1   // -4   (保留符号位)
-8 >>> 1  // 2147483644  (符号位当作数据位)

8 >> 1    // 4
8 >> 2    // 2

C 语言里有符号类型的 >> 究竟是算术还是逻辑右移,由实现定义。大多数编译器做算术右移,但不查文档就依赖这点风险很大。Go 要求移位数必须是无符号整数,对有符号/无符号类型区分得很清楚。Python 没有 >>>,因为它根本没有定宽整数。

补码:计算机怎么存负数

位只能是 0 或 1,那 -5 怎么编码?上世纪 60 年代计算机界定下来的答案是补码(two’s complement),所有现代 CPU 都这么干。

最朴素的思路是拿出一位做符号,但有两个毛病:第一,会同时存在 +0-0,别扭;第二,加减电路得检查符号位,硬件更复杂。补码同时解决了这两个问题。

规则很短:

  1. 写出正数的二进制表示
  2. 翻转每一位(这叫「反码」或「一的补码」)
  3. 加 1

动手算一遍,在 8 位补码里表示 -5

 5 的二进制:         0000 0101
 翻转所有位:         1111 1010   (这是补码里的 -6!)
 加 1:               1111 1011   ← 这是 -5

用进制转换器验证:在进制转换器里输入十进制 251,二进制输出就是 11111011。在 8 位有符号上下文里,11111011-5;在 8 位无符号上下文里,同样的位模式是 251。位一模一样,解释不同。

这也解释了前面 ~5 = -6 的反直觉。位非运算给你反码,补码等于反码加 1,所以:

~x    = -(x + 1)    // 任何用补码的语言都成立
~5    = -6
~(-3) = 2

对 n 位有符号整数,可表示范围是 -2ⁿ⁻¹2ⁿ⁻¹ − 1。8 位有符号能覆盖 -128127,32 位有符号覆盖大约 -21 亿+21 亿

九个必会的位操作模式

这九个模式大致覆盖你会写的 95% 的位操作。记下来,你会在系统代码里到处看见它们。

设置某一位:x | (1 << n)

打开第 n 位,其他位不变。

let flags = 0b0100;
flags = flags | (1 << 0);  // 0b0101

清除某一位:x & ~(1 << n)

关闭第 n 位,其他位不变。~(1 << n) 是一个除了第 n 位外所有位都置 1 的掩码。

let flags = 0b0111;
flags = flags & ~(1 << 1);  // 0b0101

翻转某一位:x ^ (1 << n)

不管当前状态,翻转第 n 位。

let flags = 0b0100;
flags = flags ^ (1 << 2);  // 0b0000
flags = flags ^ (1 << 2);  // 0b0100 变回来

检查某一位:(x >> n) & 1

n 位为 1 则返回 1,否则返回 0。等价写法:(x & (1 << n)) !== 0

const flags = 0b0101;
const isBit2Set = (flags >> 2) & 1;  // 1

提取最低置位:x & -x

只保留 x 最右边那个 1。秘诀在于补码下 -x 等于 ~x + 1,把最低置位及以下的位全部翻转。

const x = 0b10110100;
const lowest = x & -x;  // 0b00000100 = 4

这是 Fenwick 树(树状数组)实现 O(log n) 前缀和的核心技巧。

统计置位数(popcount)

数一个整数里有几个 1 位。多数语言现在都有原生函数:

// JavaScript(手写版本)
const popcount = (n) => {
  let count = 0;
  while (n) { count += n & 1; n >>>= 1; }
  return count;
};
popcount(0b10110100);  // 4
# Python 3.10+
(0b10110100).bit_count()  # 4
// Go
import "math/bits"
bits.OnesCount(0b10110100)  // 4

不用临时变量的 XOR 交换

经典面试题:不借第三个变量交换两个整数。生产代码千万别用(比用临时变量慢,而且如果 ab 指向同一内存还会出错),但值得理解。

let a = 5, b = 9;
a = a ^ b;  // a = 5 ^ 9
b = a ^ b;  // b = (5 ^ 9) ^ 9 = 5
a = a ^ b;  // a = (5 ^ 9) ^ 5 = 9
// a = 9, b = 5

判断 2 的幂:(x & (x - 1)) === 0

2 的幂只有一个位是 1。减 1 会把那位关掉、下面所有位置 1。AND 之后只有 2 的幂(和 0)得到零,所以加一个 x > 0 保护。

const isPow2 = (x) => x > 0 && (x & (x - 1)) === 0;
isPow2(16);  // true
isPow2(17);  // false

快速判奇偶:x & 1

在某些语言里比 x % 2 稍快,编译器优化后往往持平。热循环里或者就是在想位的时候值得用。

const isOdd = (x) => (x & 1) === 1;

真实代码里的位掩码标志

位运算从技巧升级到实践,就看这一节。

用 32 位打包 32 个布尔

别再写 32 个字段的布尔结构体,把它们塞进一个整数:

const FLAGS = {
  DARK_MODE:      1 << 0,
  NEW_NAV:        1 << 1,
  AI_SUGGESTIONS: 1 << 2,
  BETA_EDITOR:    1 << 3,
  // ... 一直到 1 << 31
};

let userFlags = 0;
userFlags |= FLAGS.DARK_MODE | FLAGS.AI_SUGGESTIONS;  // 启用

if (userFlags & FLAGS.AI_SUGGESTIONS) {
  showSuggestions();
}

userFlags &= ~FLAGS.DARK_MODE;  // 关闭

32 个布尔压进 4 字节,任意子集查询只要一次 AND。数据库尤其喜欢这种模式,一列替代 32 列。

Unix 文件权限

chmod 755 本质就是位运算。三个八进制数字对应三组三位:

7 = 111  (owner:   rwx)
5 = 101  (group:   r-x)
5 = 101  (others:  r-x)

试一下:打开进制转换器,源进制选八进制,输入 755,二进制输出是 111101101。文件系统就是这么存权限字段的。

加上「组写」权限:

const perms = 0o755;
const withGroupWrite = perms | 0o020;  // 0o775

IP 子网掩码

给定 192.168.1.10/24,用 AND 掩码提取网络地址:

const ip      = 0xC0A8010A;  // 192.168.1.10
const mask    = 0xFFFFFF00;  // 255.255.255.0(/24)
const network = ip & mask;   // 0xC0A80100 = 192.168.1.0

打包 ID:Snowflake

Twitter 的 Snowflake 把时间戳、机器 ID、序列号打包进一个 64 位整数:

┌─ 1 位 ─┬─── 41 位 ───┬─ 10 位 ─┬─ 12 位 ─┐
│  符号  │   时间戳    │ 机器 ID │  序列   │
└────────┴─────────────┴─────────┴─────────┘

编码一个 ID 只要两次移位和两次 OR:

const id = (BigInt(timestamp) << 22n) |
           (BigInt(machineId) << 12n) |
            BigInt(sequence);

解码反过来,右移加掩码。要对比什么时候该用 Snowflake、什么时候用 ULID 或 UUIDv7,看分布式 ID 对比

各语言的坑

JavaScript:32 位强转陷阱

JavaScript 会把操作数强转成 32 位有符号整数再做位运算,结果再转回 Number。任何超过 2³¹ − 1 = 2147483647 的值都会溢出:

2147483647 | 0   // 2147483647   (还好)
2147483648 | 0   // -2147483648  (溢出了!)
4294967295 | 0   // -1           (全 1 被解释为有符号)

要做 64 位运算得用 BigInt,它有独立的位运算符,没有位宽限制:

(2n ** 40n) | 1n  // 1099511627777n

运算符优先级 bug

这是现实中最常见的位运算 bug 之一:

// 有 bug:实际被解析为 (x & (1 == 0)),因为 == 优先级高于 &
if (x & 1 == 0) { /* ... */ }

// 正确:加括号
if ((x & 1) == 0) { /* ... */ }

C、JavaScript、Python、Go 等主流语言里,比较运算符优先级都高于位 AND/OR/XOR。拿不准就加括号。

语言对照表

语言位宽强转负数 >>BigInt 支持
JavaScript强制 32 位有符号;>>> 是无符号算术BigInt 有独立运算符
Python任意精度,无定宽算术原生
Go严格,移位数必须无符号有符号类型走算术math/big
C/C++随类型;intunsigned有符号时实现定义无内置
Rust严格,debug 下溢出 panic有符号类型走算术u128 或外部 crate

Python 的无限宽度陷阱

Python 整数没有固定位宽,补码逻辑概念上向左延伸到无穷。这就是为什么 ~5-6(不是 25065530):Python 把结果当作负整数,不是定宽位模式。如果你想要环绕语义,显式加掩码:

# 模拟 8 位 NOT
(~5) & 0xFF  # 250

2026 年的性能现实

常见说法是位运算「永远更快」。2026 年的真相是一半对一半错。

编译器早就会做显而易见的重写。现代优化器会自动把 x * 2 改成 x << 1。应用代码里刻意写 x << 1 图速度是 cargo cult 式的性能调优,没帮上忙,可读性倒是崩了。

位运算真正赢下来的场景:

  • 数值密集的热循环:popcount、前导/尾随零计数、位棋盘国际象棋引擎
  • 紧凑数据结构:Bloom filter、roaring bitmap、Fenwick 树
  • 硬件寄存器与内存映射 I/O:嵌入式、内核、固件
  • 密码学原语:AES、ChaCha20、SHA,底子就是 XOR、循环移位、shift
  • 压缩解压:Huffman 编码、行程编码、打包整数
  • 数据库引擎:位图索引、Parquet 字典编码等打包列存格式

位运算帮不上忙的场景:在每个请求跑两遍的业务逻辑函数里把 x % 2 改成 x & 1。加速测不出来,可读性代价倒是实在。

位操作永远赢的一点是内存占用。32 个 flag 打包成一个 int 比 32 个布尔省 31 字节。规模上去后,几百万用户、上亿事件,就是缓存友好与缓存雪崩的差别。

速查表

操作运算符示例结果典型用途
AND&0b1100 & 0b10100b1000掩码/提取
OR|0b1100 | 0b10100b1110合并标志
XOR^0b1100 ^ 0b10100b0110翻转/检测差异
NOT~~0b1100...11110011构造反向掩码
左移<<1 << 38乘以 2ⁿ
右移>>16 >> 24除以 2ⁿ(带符号)
无符号右移(JS)>>>-1 >>> 04294967295按无符号解释
设位 n|x | (1 << n)打开位
清位 n& ~x & ~(1 << n)关闭位
翻位 n^x ^ (1 << n)翻转位
查位 n&(x >> n) & 101测试位
最低置位& -x & -x提取最低位
判 2 的幂&x > 0 && (x & (x-1)) == 0布尔检测 2 的幂

FAQ

逻辑 AND(&&)和位 AND(&)有什么区别?

逻辑 AND 作用在布尔值上并会短路,条件判断用它;位 AND 作用在整数的每一位上,两边都会求值,做位掩码时才用。

为什么 ~1 在大多数语言里是 -2

位非会翻转所有位得到反码,而补码表示下 ~x 恒等于 -(x + 1),所以 ~1 = -2~0 = -1~(-1) = 0,规律一致。

x << 1 真的比 x * 2 快吗?

现实里不会。现代编译器会把 x * 2 自动优化成同样的移位指令,可读性优先写 x * 2;只在构造位掩码等真正在想位的场景用 <<

JavaScript 支持 64 位位运算吗?

普通 &|^<<>> 不支持,它们会强转成 32 位有符号整数;要做 64 位以上运算必须用 BigInt 字面量,例如 1n << 40n

怎么高效统计置位数?

直接用语言内置函数:Go 的 bits.OnesCount、Java 的 Integer.bitCount、Python 3.10+ 的 .bit_count(),在 x86/ARM 上会编译成单条 POPCNT 指令。

什么时候该用位掩码标志,什么时候该用布尔结构体?

位掩码适合紧凑存储大量标志(数据库、网络协议、文件格式)和快速组合测试;字段类型不同或代码可读性比内存更重要时,优先用结构体。

移位量超过位宽会怎样?

C/C++ 里是未定义行为;JavaScript 里移位量对 32 取模,所以 1 << 32 等于 1;Python 没有位宽,1 << 100 仍是合法大整数,别依赖超宽行为。

为什么 Python 的 ~5-6 而不是 2

Python 整数没有固定位宽,补码概念向左延伸到无穷,所以 ~5 等于 -(5 + 1) = -6;要得到 8 位反转后的 250,自己加 & 0xFF 掩码即可。

XOR 加密安全吗?

XOR 加密只有用与明文等长的真随机一次性密钥时(OTP)才安全;短密钥循环的所谓「XOR 加密」轻易可破,AES、ChaCha20 里的 XOR 只是众多步骤之一。

怎么手算一个负数的补码?

按目标位宽写出正数二进制,翻转每一位,再加 1。例:8 位 -5 = 000001011111101011111011,用进制转换工具验证 251 即可。

相关工具和延伸阅读