☰
异或运算深度解析:从真值表到工程实战的位运算指南
2026/10/10 3:16:50 网站建设 项目流程

异或运算(XOR)大概是位运算里最被低估的一个。很多同学刚开始学编程的时候,会把&、|、^一带而过,觉得“也就是算算奇偶校验而已”。但代码写得久了你会发现,异或几乎是位运算里最优雅的一种:它能不用临时变量交换两个数,能在 O(n) 时间里从一堆数字里找出唯一出现奇数次的那个数,还能在加密、校验、哈希、硬件电路里反复出现。这篇内容不是教科书复读,我直接从使用者的角度把它拆开——先建立直觉,再给一堆可以直接跑的代码,最后把我踩过的坑也一并交代清楚。适合刚开始接触位运算的开发者,也适合想系统补一补底层思维的老手。

1. 异或运算到底是什么:从真值表到直觉理解

1.1 真值表与三条铁律

异或的符号是^,英文全称 XOR(exclusive OR)。它的运算规则只有一句话:两个比特位相同为 0,不同为 1。真值表写出来就是四行:

输入 A输入 BA ^ B
000
011
101
110

很多人第一次看到这张表,觉得“这不就是个不一样就输出1”的门吗,好像也没什么。但真正让异或有用的,是它可以推导出三条铁律:

  1. 归零律:x ^ x = 0,任何一个数和自身异或,结果一定是 0。
  2. 恒等律:x ^ 0 = x,任何一个数和 0 异或,结果还是它自己。
  3. 交换律和结合律:x ^ y = y ^ x,(x ^ y) ^ z = x ^ (y ^ z),异或不关心顺序,分组也不影响结果。

前两条尤其重要。x ^ x = 0意味着“成对的东西可以互相抵消”,这是后面所有异或技巧的根。x ^ 0 = x意味着“和 0 打交道不会改变原值”,这是异或能用来做“补位”和“还原”的基础。

如果你实在记不住,可以用生活里的例子理解:问一个人“今晚吃火锅还是吃烧烤”,如果只能二选一,那结果就是异或——选火锅和选烧烤是两个不同状态,同一个人不可能同时选两个。“两个都吃”属于普通或(OR)的结果,不是异或。这个类比能帮不少初学者分清 XOR 和 OR。

1.2 为什么“可逆”这么值钱:信息不丢失

异或最特别的地方在于它有一个性质:x ^ y ^ y = x。一个数对另一个数异或两次,就会回到原点。换句话说,异或运算不丢信息。

这一点和加减法很像。加法和减法互为逆运算,你知道a + b = 10,又知道其中一个数是 3,另一个数就一定是 7。异或也一样,你知道a ^ b = c,那么用c和a异或就能还原出b,用c和b异或就能还原出a。

把这个性质放到场景里就太重要了:加密的时候,明文和密钥异或得到密文,密文再和同一个密钥异或就能还原明文;传输数据的时候,发送方把数据算出一个校验字节,接收方把整个包再异或一遍,结果是 0 就代表没出错。所有这类应用,本质上都是吃“可逆”这碗饭。

顺带说一个硬件层面的冷知识:异或其实就是加法器的基础。二进制加法在最低一位不考虑进位时,0+0=0,0+1=1,1+0=1,1+1=0(进位 1 被忽略),这和异或的真值表完全一致。所以 CPU 里的加法器,第一步永远是一个异或门。你每天写的代码,最后落到硬件上,都是在用一堆异或门做事情。

2. 为什么要关注异或:它在真实世界里无处不在

2.1 从硬件电路到通信协议里的校验

异或在底层世界是个“劳模”。任何一位二进制加法器的低位输出都是异或门,计算机里的 ALU(算术逻辑单元)布满这种电路。这不是我为了拔高异或的身份硬扯的,而是异或天然的“半加”特性决定的。

通信协议里,异或校验更是常见。很多串口设备、I2C 通信、CAN 总线,甚至 TCP/IP 的校验和运算里都有异或的影子。原因是它实现成本极低,计算速度快,而且对于随机单比特翻转特别敏感。

举个例子,一个传感器上报一帧数据,内容是一个四字节数组[0x01, 0x02, 0x03, 0x04]。发送端把四个字节依次异或:

0x01 ^ 0x02 = 0x03 0x03 ^ 0x03 = 0x00 0x00 ^ 0x04 = 0x04

于是校验字节就是0x04,把它追加在数据后面一起发出去。接收端拿到完整五个字节后全部异或,如果结果是 0,就认为数据大概率没被改动:

0x01 ^ 0x02 ^ 0x03 ^ 0x04 ^ 0x04 = 0x00

这个思路在嵌入式开发里几乎天天用。你不需要一个复杂的 CRC 库,只需要一行异或,就能挡住大多数噪声干扰。

2.2 加密、哈希和随机数:异或是“胶水”

现代对称加密算法,核心运算都离不开异或。AES 这种高级加密标准,它的轮函数里也会大量使用异或,把明文和轮密钥混合。原理非常简单:明文和密钥异或得到密文,密文再和同一个密钥异或恢复明文,因为自反性,这事情天然适合做加解密。

哈希算法里,异或也是个高频操作。布隆过滤器用多个哈希函数把元素映射到位数组,更新时经常用异或来混合;一致性哈希里也会用异或把 key 映射到环上;感知哈希(pHash)算图片指纹时,最终也是得到一个位向量。为什么大家都在用异或?因为它是一个“低成本混合器”:输入两个位,输出一个位,既能混合信息,又保留可还原性。

随机数领域同样有它的身影。有一种非常轻量的伪随机数生成器叫 xorshift,核心就是反复做异或和移位,几十行代码就能跑出看起来挺像样的随机序列。很多嵌入式环境里没有标准库的随机函数,用异或移位生成一个够用的伪随机数,是再常见不过的做法。

从工程视角看,异或就是一个“万能胶水”:它能混信息,能还原信息,能抵消信息,而且快得离谱。你掌握它的这些性格之后,遇到很多问题第一反应就会想到它。

3. 实战一:经典位操作技巧与代码实现

3.1 不用临时变量交换两个数:原理、代码、警告

这是异或最出名的“魔术”。三个异或就能交换两个变量的值,不需要第三个临时变量:

a = a ^ b; b = a ^ b; a = a ^ b;

推导过程其实很简单,我用数学式给你拆一遍。假设一开始a的原始值是 A,b的原始值是 B。

第一步后,a变成A ^ B。
第二步,b = a ^ b = (A ^ B) ^ B = A ^ (B ^ B) = A ^ 0 = A。
第三步,a = a ^ b = (A ^ B) ^ A = B ^ (A ^ A) = B ^ 0 = B。

看出来了吧,两次自反性抵消,两个数就在完全没有第三个变量的情况下换过来了。

换成 Python 也是一样的:

a = 5 b = 8 a ^= b b ^= a a ^= b print(a, b) # 8 5

但我必须在这里给所有人提个醒:生产代码里不要用这个技巧交换变量。原因有两条。

第一,如果a和b指向同一个内存地址,比如数组里的同一个元素,执行a ^= b之后这个位置直接变成 0,因为x ^ x = 0,后面就全乱了。这个 bug 非常隐蔽,尤其在 C 语言里处理数组交换时容易踩中。

第二,现代编译器的优化能力很强,临时变量交换会被编译成足够高效的指令,甚至用寄存器重命名直接省掉移动操作。XOR 交换在旧硬件上可能省一点点内存,现在基本没有性能优势,反而牺牲可读性。

所以我的态度是:这个技巧用来理解异或特别棒,但别当炫技工具到处用。理解它,然后忘掉它,才是最佳姿势。

3.2 找出出现奇数次的数字

这是异或最经典的一道面试题,也是很多人的“异或启蒙题”。题目描述大概是:一个非空整数数组里,只有一个数出现了一次,其他所有数都出现了两次,请找出这个只出现一次的数。

解法就是一个循环,把所有数异或起来:

def single_number(nums): result = 0 for n in nums: result ^= n return result

为什么能成?因为异或满足交换律和结合律,成对出现的相同数字会两两抵消成 0,最后剩下的就是唯一出现一次的那个数。比如数组[2, 3, 2, 4, 4],计算过程是:

2 ^ 3 ^ 2 ^ 4 ^ 4 = (2 ^ 2) ^ (4 ^ 4) ^ 3 = 0 ^ 0 ^ 3 = 3

时间复杂度 O(n),空间复杂度 O(1),这是一个“既快又省”的解法。很多人看完答案觉得很妙,但如果你只是背下套路,遇到变体就废了。

比如题目改成:有两个数各出现一次,其他数都出现两次,怎么找这两个数?思路是先全数组异或一遍,得到a ^ b的结果x。因为a != b,所以x一定至少有一位是 1,这一位表示a和b在该位不同。按这一位是 0 还是 1 把数组分成两组,每组再各自异或,就能分别得到两个数。这个扩展才是真正考验你有没有理解“异或本身是奇偶性检测器”这件事。

3.3 用异或翻转特定位、构建掩码

异或的另一个日常用途是翻转指定位。比如你要把某个整数的第 k 位从 0 变成 1,或从 1 变成 0,可以直接:

x ^= (1 << k)

原理是1 << k构造了一个只有第 k 位为 1 的掩码,其他位都是 0。任何位和 0 异或保持原样,只有第 k 位和 1 异或发生翻转。这一招在状态位、标志位的控制里非常实用。

把常见的位操作放一起对比,你会更清楚异或的定位:

操作方法特点
置位(设为 1)x | (1 << k)用或运算,强制变为 1
清位(设为 0)x & ~(1 << k)用与运算和非,强制变为 0
翻转位x ^ (1 << k)用异或,原来是 0 变 1,原来是 1 变 0
判断位是否为 1x & (1 << k)用与运算,结果为 0 则该位为 0

判断两个数是否相等,也可以借助异或:(a ^ b) == 0。判断两个数符号位是否相同,可以看(a ^ b) >> (bit_width - 1)的最高位,如果相同符号,异或后最高位是 0,否则是 1。这比分别判断两个数再比较更简洁,而且在一些追求极致的代码里确实有人这么写。

4. 实战二:异或在算法与数据结构中的应用

4.1 内存优化版双向链表

传统双向链表每个节点要存两个指针:prev和next。在嵌入式等内存受限的环境里,一个指针 4 字节或 8 字节,两个指针就是双倍内存。异或链表(XOR Linked List)只存一个指针字段:prev ^ next。

遍历的时候,你已知当前节点的地址cur和上一个节点的地址prev,那么下一个节点就是:

next = prev ^ cur->link;

因为link里存储的是prev ^ next,所以prev ^ (prev ^ next) = next,利用的又是自反性。

用 C 风格的伪码表示就是:

struct xor_node { int value; uintptr_t link; // prev ^ next };

插入和删除节点时,需要同时更新相邻节点的link字段。这个技巧在原理上非常迷人,但它有个致命前提:你必须要能拿到对象的原始地址,并且能对地址做按位异或。在 C/C++ 里可以用uintptr_t强转指针来做,在 Python、Java、JavaScript 这类带垃圾回收的语言里,对象地址根本不被允许这样操作,所以这个技巧实际生产用得很少,更多是面试题和嵌入式课程里的思维训练。

我还是那句话:理解它的原理,能让你明白地址和位运算之间的关系,但别指望在日常业务代码里靠它省内存。

4.2 感知哈希与海明距离:用异或算图片相似度

图像感知哈希(pHash)是一个很实用的场景。大概流程是:把图片缩小到固定尺寸,转成灰度图,做离散余弦变换(DCT),取低频分量,最后生成一串 64 位或 128 位的指纹。判断两张图片是否相似,就是比较两个指纹的海明距离——对应位不同的数量。

海明距离怎么算?最直接的办法就是把两个指纹都转成整数,异或一下,然后数结果里 1 的个数:

def hamming_distance(a: int, b: int) -> int: x = a ^ b count = 0 while x: x &= x - 1 # 每次消掉最低位的1 count += 1 return count

x &= x - 1是一个经典技巧,它能把当前整数最低位的 1 变成 0。每执行一次,就消掉一个 1,循环次数就是 1 的个数,效率比逐位检测高很多。

在很多 CPU 上,还有专门的指令来数 1 的个数,比如 x86 的 POPCNT 指令。C 语言里可以直接调__builtin_popcount,Python 3.8+ 可以用int.bit_count():

def hamming_distance(a: int, b: int) -> int: return (a ^ b).bit_count()

异或在这里的价值特别明显:它把“两个位向量逐位比较不同”这个运算,一次性压缩成一个整数的位运算,然后交给硬件去数。做图像去重、相似图片搜索的时候,这个思路非常高效。

4.3 图论里的欧拉路径判定:奇偶性与异或

异或本质上是一个奇偶性检测器。你把一堆数异或起来,结果最低位反映了这堆数中“二进制最低位为 1”的个数是奇数还是偶数。这个视角在图论里也有应用。

无向图是否存在欧拉路径(一笔画),充要条件是奇数度的顶点个数为 0 或 2。判定奇数度的时候,你可以用异或的思路:给每个顶点维护一个二进制标记,遍历每条边时对该顶点的度做异或累加。当然实际代码里用数组计数就够了,但异或的思维能帮你理解“奇偶性到底在表达什么”。

往更深一层说,很多和“成对抵消”相关的图论问题,都能和异或产生联系。比如在一组边里找出一条闭合路径上的所有边,也可以用异或做区分。这不是说异或能取代所有数据结构,而是提醒你:当问题里出现“两个一组的抵消”“出现奇偶次数”这些关键词时,异或应该是你第一个想到的工具。

5. 实战三:校验、固件与简单加密

5.1 XOR 校验和累加校验怎么选

我做嵌入式相关项目的时候,经常要在异或校验和累加校验之间做选择。两者实现的复杂度差不多,但侧重点不同。

异或校验的基本写法是:

uint8_t xor_checksum(const uint8_t *data, size_t len) { uint8_t sum = 0; for (size_t i = 0; i < len; i++) { sum ^= data[i]; } return sum; }

累加校验就是把异或改成加法,最后取低 8 位或 16 位。两者对比起来:

对比项异或校验累加校验
实现复杂度极低极低
计算速度快快
单比特翻转检测能检测能检测
同一位置双比特翻转会抵消漏检也可能漏检
适用场景简单串口协议、传感器帧更偏传统校验和场景

我个人的经验是:在传感器数据采集、简单设备间通信这类场景里,异或校验足够用,而且代码短。但你得清楚它的局限——如果两个比特在同一位置同时翻转,1 ^ 1 = 0,会被当成没变化。所以异或校验适合检测随机干扰,不适合强对抗或者高可靠性的场景,那种地方老老实实用 CRC。

5.2 玩具级对称加密:XOR 的极限与教训

用异或实现一个“玩具级”加密非常容易:

def xor_cipher(data: bytes, key: bytes) -> bytes: return bytes(b ^ key[i % len(key)] for i, b in enumerate(data))

加密一遍是密文,再加密一遍就还原成明文,因为中间利用的就是data ^ key ^ key = data。测试起来很简单:

plain = b"hello world" key = b"key123" cipher = xor_cipher(plain, key) print(cipher.hex()) # 密文十六进制 print(xor_cipher(cipher, key)) # b'hello world'

但如果你真的打算拿它保护数据,我劝你立刻打住。单字节循环密钥的异或加密,根本没有安全性。频率分析可以直接破解;密钥一旦重复使用,两个密文异或就能把密钥消掉,直接暴露出两个明文的异或关系。这类实现只能用于教学演示和个人娱乐,绝不能碰真实敏感数据。

那为什么现代密码学还在用异或?因为异或是“混合器”,但它必须配合强密钥扩展、混淆和扩散才能真正安全。AES 这类算法里的异或只是整个复杂机制的一个环节。理解这一点很重要:异或给了你一个积木,但积木本身不等于房子。

5.3 文件异或备份:一个很有意思的玩法

异或的自反性还可以用来做一种简单的“双文件备份”。假设你有两个文件 A 和 B,可以生成一个差异文件 X,其中每个字节都是 A 和 B 对应字节异或的结果:

def xor_files(file_a, file_b, file_x): with open(file_a, 'rb') as fa, open(file_b, 'rb') as fb, open(file_x, 'wb') as fx: while True: ba = fa.read(4096) bb = fb.read(4096) if not ba and not bb: break if len(ba) != len(bb): # 实际项目中需要先对齐长度,这里简化为按短的那个处理 pass fx.write(bytes(x ^ y for x, y in zip(ba, bb)))

之后如果文件 A 丢了,只要你有备份盘 B 和差异文件 X,就能还原:

A = B ^ X

因为B ^ (A ^ B) = A。这个思路在一些老派的“双机备份”方案里确实出现过:主盘和备份盘定期做异或生成差异盘,其中一个坏了,另一个加差异盘就能恢复。

但我要说清楚,这只是教学级的奇偶备份,不是正经的容灾方案。它只能应对单盘丢失,而且对数据完整性没有任何校验能力。真实场景里,RAID 5、纠删码这些方案要复杂得多。理解异或备份的底层逻辑能帮你理解那些方案的数学基础,但不能直接拿它替代。

6. 常见坑与排查记录:位运算里容易翻车的地方

6.1 运算符优先级是最容易踩的雷

这是我在帮别人 review 代码时见过最多的问题。不同语言里,异或^的优先级和其他运算符的关系并不一致,一个不小心就让逻辑全错。

在 C 和 Java 里,==的优先级高于^,所以:

if (a ^ b == 0) // 实际是 a ^ (b == 0),不是判断 a 和 b 是否相等

比较两个数是否相等,必须写成:

if ((a ^ b) == 0)

在 Python 里,^的优先级高于==,同样一行a ^ b == 0解释为(a ^ b) == 0,结果恰好符合直觉,反而让人更迷惑。不同语言行为不一致,这是最危险的地方。

我的建议很简单:凡涉及位运算,一律加括号,不要依赖优先级。也许你觉得代码长一点,但“不指望别人也记得优先级”本身就是一种工程素养。排查这类 bug 的时候,可以把整行拆开逐步打印,看每一小段的结果,通常很快就能定位。

6.2 逻辑异或:别把布尔当成位数字

有人喜欢用^做逻辑异或,但在某些语言里会莫名其妙“翻车”。JavaScript 是一个典型例子:5 ^ 3会把两边的值先转成 32 位有符号整数,得到6,因为二进制101 ^ 011 = 110。如果你本意是判断两个布尔值是否不同,结果却拿到了一个非 0 的数字,再配合 if 判断,行为就不直观了。

想表达逻辑异或,最稳妥的写法是:

function logicalXor(a, b) { return Boolean(a) !== Boolean(b); }

Python 里布尔运算相对好一点,True ^ False的结果是True,bool类型直接支持异或。但这种写法可读性依然不高,我建议你自己写一个命名清晰的函数,比每次用^猜语义强。

6.3 符号位、字节宽度和数据长度

异或操作看着简单,但在不同语言里对“整数到底有多少位”的处理不一样,很容易出问题。

在 C 语言里,int通常是 32 位有符号整数。如果做校验和时数据里有大于 127 的字节,你用的是int接收,异或结果可能受符号位扩展影响,最后转成uint8_t时才截断。解决办法是全程用uint8_t或unsigned char类型,或者在存入时强制& 0xFF。

在 Python 里,整数是无限精度的,负数异或后的二进制表示会是一长串补码形式。新手打印(-1) ^ 0时会看到很多f,是因为 Python 显示负数的无限长补码。如果想把它当无符号整数处理,需要主动掩码:

result = (-1) ^ 0 print(result & 0xFFFFFFFF) # 4294967295

在 JavaScript 里,^会先把操作数转成 32 位整数,处理大数时会丢失精度。比如0xFFFFFFFF ^ 0,结果不是你想的0xFFFFFFFF,而是-1。遇到这种情况要用 BigInt,并在之后注意位运算的规则。

这些细节看起来琐碎,但在做协议解析、固件校验、哈希指纹时,一旦数据宽度对不上,排查起来非常痛苦。我的经验是:先明确数据类型和位宽,再写任何一行位运算。

6.4 关于性能的误区

很多人一提起位运算就默认“更快”,其实并不绝对。在 C/C++ 这类编译型语言里,异或确实是一条指令,非常快。在 Python、JavaScript 这类解释型语言里,位运算涉及类型转换和对象分发,性能并没有你想象中那么神话。

举个例子,数一个整数二进制里有多少个 1,很多人还在手写循环:

count = 0 while x: x &= x - 1 count += 1

但在 Python 3.8 以后,直接调内置方法反而更快:

count = x.bit_count()

所以我的建议是:用异或解决问题,是因为它在数学逻辑上简洁,而不是为了追求微秒级的性能。真到了性能敏感的核心代码,先用 profiler 量一量,别凭感觉优化。

另一个常见的性能误伤是:为了“少写一个变量”用异或交换两个数,结果现代编译器下反而可能更慢。可读性降低,性能没提升,两头不讨好。

7. 最后再分享一点实操经验

我自己在多个项目里用过异或:传感器数据帧的校验、固件差分对比、图片去重时算海明距离、数据同步时生成差异文件。每一次用到它,都是因为场景里出现了“成对抵消”“可逆还原”“奇偶检测”这几个关键词。它不是什么高深魔法,就是一个被很多教材草草带过、但实际价值极高的基础运算。

如果让我给新手一个练习路径,我会说:先真值表,再亲手推一遍交换两个数的例子,然后去做那道 Single Number 的经典题,最后尝试自己写一个异或加密再破解它。这几步走完,你对异或的直觉就建立起来了。至于面试炫技式的位运算,能看懂就行,真正在工程里写出别人都能维护的代码,才是更重要的事。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询