位运算高级技巧:交换变量、判断奇偶、求绝对值、2的幂次判断

位运算这东西,用好了是真顺手。我做了十几年嵌入式,很多场景下,位运算就是那把「快刀」。今天咱们聊四个高频技巧:交换变量、判断奇偶、求绝对值、判断2的幂次。别看它们简单,用对了地方,代码效率能上一个台阶。

1. 交换变量:不用临时变量,三行搞定

先看最经典的——用异或交换两个整数。你可能会问:为什么要这么折腾?直接定义一个temp不就行了?

嗯,在大多数场景下,用临时变量确实没问题。但我在做底层驱动时,遇到过寄存器资源极度紧张的情况,能省一个变量就省一个。这时候异或交换就派上用场了。

// 传统写法
int temp = a;
a = b;
b = temp;

// 位运算写法
a ^= b;
b ^= a;
a ^= b;

原理其实不复杂:异或运算满足交换律和结合律,而且任何数和自己异或等于0,任何数和0异或等于自身。你拿笔算一遍就明白了。

注意: 如果a和b指向同一块内存地址,这个写法会出问题。比如你传了同一个变量的引用进来,结果会被清零。我曾经在代码审查时抓到过这种bug,排查了半天才发现是这里埋的雷。

2. 判断奇偶:比取模快一个数量级

判断一个数是奇数还是偶数,新手喜欢写 n % 2 == 0。但说实话,在嵌入式世界里,取模运算是个「重活」。CPU做除法比做位运算慢得多。

用位运算怎么写?看最低位就行了。

// 判断偶数
if ((n & 1) == 0) {
    // 偶数
}

// 判断奇数
if ((n & 1) == 1) {
    // 奇数
}

为什么?因为二进制的最低位如果是0,那这个数就是偶数;如果是1,就是奇数。你想想看,任何偶数都能写成2k的形式,最低位必然是0。这个技巧我在做定时器中断处理时经常用,每次判断奇偶周期,省下来的CPU周期可以干别的事。

小提示: 负数也一样适用。补码表示下,负奇数的最低位也是1。所以这个判断对int、long都有效。

3. 求绝对值:避开分支预测的坑

求绝对值,最直接的写法是 abs(x) 或者三目运算符 x > 0 ? x : -x。但这里面有个隐藏问题——分支预测失败。

在流水线很深的现代CPU里,分支预测失败一次,代价是十几个时钟周期。如果你在一个循环里频繁调用abs,性能损耗不容忽视。

位运算版本长这样:

int abs_bit(int x) {
    int mask = x >> (sizeof(int) * 8 - 1);
    return (x + mask) ^ mask;
}

我来拆解一下:

  • x >> 31:如果x是正数,得到0;如果是负数,得到-1(全1)。
  • x + mask:正数不变,负数相当于减1(因为mask是-1)。
  • ^ mask:正数不变,负数相当于取反。

合起来的效果就是:正数不变,负数取反加1——这不就是负数的补码转原码吗?

核心思想: 用算术右移生成掩码,然后通过加法和异或完成绝对值运算。全程无分支,流水线不会被清空。

不过我得提醒一句:这个写法对INT_MIN会出问题。因为INT_MIN的绝对值超出了int的表示范围。我在做音频信号处理时遇到过这个边界情况,后来加了个保护判断才搞定。

4. 判断2的幂次:一行代码搞定

判断一个数是不是2的幂次,比如1、2、4、8、16……这个需求在内存分配、缓冲区大小校验里很常见。

常规写法是循环除以2,但效率太低。位运算版本简洁到令人发指:

bool is_power_of_two(unsigned int n) {
    return n && !(n & (n - 1));
}

为什么能这么写?因为2的幂次在二进制里只有一个1。比如8是1000,7是0111。两者按位与,结果是0。反过来,如果n不是2的幂次,二进制里至少有两个1,n & (n-1) 就不会是0。

前面的 n && 是为了排除0的情况——0不是2的幂次,但0 & (0-1) 也是0,所以得单独处理。

应用场景: 我在做DMA缓冲区对齐检查时,就用这个判断来验证用户传入的缓冲区大小是不是2的幂次。如果不是,直接返回错误,避免后续的内存访问异常。

知识体系总览

下面这张图把今天讲的四个技巧串起来了。你可以看到,它们背后都依赖同一个核心思想——用位运算替代算术运算和分支判断。

位运算高级技巧知识体系 位运算核心思想 交换变量 判断奇偶 求绝对值 2的幂次判断 核心思想:用位运算替代算术运算和分支判断,提升执行效率

避坑指南

最后,我把自己踩过的坑总结一下:

  • 交换变量别用在同一内存地址上——结果会归零,我吃过这个亏。
  • 求绝对值注意INT_MIN——这个边界值会溢出,加个保护判断更稳妥。
  • 判断2的幂次别忘了排除0——0不是2的幂次,但位运算结果会误判。
  • 有符号数的右移是算术右移还是逻辑右移——C标准没强制规定,但大多数编译器对 signed int 做算术右移。写跨平台代码时要注意。

好了,这四个技巧够你玩一阵子了。下次写代码时,遇到这些场景,试试位运算版本,你会感受到那种「行云流水」的快感。