3、位翻转与异或:用异或(^)翻转特定位,用异或实现无临时变量交换

异或运算,在C语言里用 ^ 表示。很多初学者觉得它就是个位运算里的“小透明”,平时用得少。但我得说,这玩意儿在嵌入式底层开发里,绝对是“扫地僧”级别的存在。

我个人习惯把异或叫做“可控翻转器”。为什么?因为它有一个特别直观的特性:一个位与0异或,保持不变;与1异或,直接取反。你想想看,这不就是“指哪儿打哪儿”吗?

3.1 异或的核心特性

先复习一下异或的真值表。其实就一句话:相同为0,不同为1

ABA ^ B
000
011
101
110

基于这个表,我们能推导出三个非常重要的性质:

  • 归零律a ^ a = 0。自己跟自己异或,结果一定是0。
  • 恒等律a ^ 0 = a。跟0异或,原封不动。
  • 交换律与结合律a ^ b = b ^ a(a ^ b) ^ c = a ^ (b ^ c)

这些性质看着简单,但组合起来威力巨大。下面我们一个个看。

3.2 用异或翻转特定位

这是异或最直接的应用。比如你想把一个寄存器的第3位和第5位翻转,其他位保持不变。用异或,一行代码搞定。

// 假设 reg 是某个8位寄存器
uint8_t reg = 0b10101010;

// 翻转第3位和第5位(从0开始计数)
reg ^= (1 << 3) | (1 << 5);

// 结果:0b10001010

为什么?因为 (1 << 3) | (1 << 5) 构造了一个掩码,只有第3位和第5位是1,其他位都是0。跟这个掩码异或,等于只对这两个位执行了取反操作。

核心公式翻转特定位 = 原值 ^ 掩码,其中掩码的1对应要翻转的位。

我在项目中遇到过这样一个场景:控制一个LED灯带,需要每隔一段时间翻转某个颜色通道的极性。如果用读-改-写的方式,还得先读寄存器,再与或非,至少三步。用异或,一步到位,而且不会影响其他位。这在时间敏感的ISR(中断服务程序)里特别有用。

3.3 用异或实现无临时变量交换

这个技巧,说实话,面试题里出现频率极高。但实际工程中,我建议你谨慎使用。不过理解它的原理,对加深异或的理解非常有帮助。

传统的交换变量,需要第三个临时变量:

int tmp = a;
a = b;
b = tmp;

用异或,可以这样写:

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

我们来拆解一下这三行代码,看看它为什么能交换。

假设初始值:a = 5b = 3

  1. 第一行a = a ^ ba = 5 ^ 3 = 6。此时a变成了“混合值”。
  2. 第二行b = a ^ bb = 6 ^ 3 = 5。注意,这里的a是6,b是3。6 ^ 3 = 5,正好是原来的a值。b成功拿到了a的原始值。
  3. 第三行a = a ^ ba = 6 ^ 5 = 3。此时a是6,b是5。6 ^ 5 = 3,正好是原来的b值。交换完成。

记忆口诀:三行异或,两两相消。第一行存混合,第二行还原a,第三行还原b。

3.4 避坑指南:为什么我不推荐在生产代码里用异或交换

嗯,这里要注意。虽然异或交换看起来很酷,但我在实际项目中吃过它的亏。

我曾经在调试一个电机驱动代码时,用了异或交换两个变量。结果发现,当 ab 指向同一个内存地址时(比如 swap(&x, &x)),第一行 a = a ^ b 会把变量直接清零。因为 ab 是同一个东西,a ^ a = 0。后面两行再怎么异或,结果都是0。

结论:如果两个变量可能指向同一地址,千万别用异或交换。老老实实用临时变量,编译器会帮你优化好的。

另外,从可读性角度讲,异或交换不如临时变量直观。你想想看,三个月后你回来看代码,看到三行异或,还得在脑子里推导一遍才知道是交换。而 tmp = a; a = b; b = tmp; 一目了然。

3.5 知识体系:异或操作的核心逻辑

下面这张图,帮你理清异或操作在嵌入式开发中的核心应用脉络。

异或运算 (^) 核心特性 • 归零律:a ^ a = 0 • 恒等律:a ^ 0 = a • 交换律:a ^ b = b ^ a 应用1:位翻转 • 掩码中1对应翻转位 • 0对应保持不变 • 常用于寄存器操作 应用2:变量交换 • 三行异或实现交换 • 无需临时变量 • 注意同地址陷阱 ⚠️ 注意事项 • 异或交换不适用于 a 和 b 指向同一地址的情况 • 生产代码建议优先使用临时变量,可读性更好

3.6 实战:寄存器位翻转的完整示例

假设你有一个8位的GPIO输出寄存器,你想每隔100ms翻转第1位和第6位的状态,用来控制两个LED的闪烁。用异或,代码非常简洁。

#include <stdint.h>
#include <unistd.h>  // 假设有usleep

// 模拟GPIO输出寄存器
volatile uint8_t gpio_out = 0x00;

// 翻转第1位和第6位
#define TOGGLE_MASK  ((1 << 1) | (1 << 6))

void toggle_leds(void) {
    gpio_out ^= TOGGLE_MASK;
}

int main(void) {
    while (1) {
        toggle_leds();
        // 假设 usleep(100000) 延时100ms
        usleep(100000);
    }
    return 0;
}

这段代码里,gpio_out ^= TOGGLE_MASK 是核心。它只翻转第1位和第6位,其他位纹丝不动。而且它是原子操作(在大多数MCU上,对8位变量的异或赋值是一条指令),不会出现读-改-写之间的中断干扰问题。

小技巧:如果你需要根据条件来决定是否翻转某个位,可以这样写:reg ^= (condition) ? mask : 0;。条件为真时翻转,为假时不变。

3.7 总结

异或操作,说白了就是“可控翻转”。它的核心价值在于:用掩码精确控制哪些位翻转,哪些位不变。这在寄存器操作、状态机切换、校验计算等场景中非常实用。

至于无临时变量交换,我建议你把它当作一个理解异或性质的练习题,而不是生产代码的常规手段。毕竟,代码是写给人看的,顺便给机器执行。可读性永远比炫技重要。

好了,这一章就到这里。记住异或的三个性质和两个应用场景,你在嵌入式底层开发中会越来越得心应手。


公众号:蓝海资料掘金营,微信deep3321