CC++ & Algorithm

一键反转的开关:为什么 `a ^ b ^ a` 总能变回你想要的答案?

调试了一个下午的加密逻辑,忽然发现数据还原不回来。我盯着那一行 data ^= key,心想这算法明明写对了啊,怎么解密出来全是乱码?直到我在纸上老老实实推了一遍二进制,才猛地意识到问题不在密钥,而在表达式优先级太任性。就是那一次,我把 ^~ 这两个位运算符彻底刻进脑子里。


先说异或:一种“找不同”的比赛

按位异或的规则一句话:相同为0,不同为1。你可以把它想象成两个选手出拳,出一样的动作就平局(0),出不一样的才算得分(1)。

比如 6 ^ 3,二进制是 110 ^ 011

  110
^ 011
-----
  101

结果 101,也就是 5。逐位对比,第0位 0 和 1 不同,得 1;第1位 1 和 1 相同,得 0;第2位 1 和 0 不同,得 1。

这比你想的要有用得多。最经典的一个性质:一个数异或两次另一个数,会回到自己。公式写出来就是 a ^ b ^ b == a。像电视遥控器上的静音键,按一下声音消失,再按一下声音恢复,中间被“异或”了两次,状态又回到最初。

这个性质在编程里最常见的使用场景是:

  • 简单加密:cipher = data ^ key,解密时再 cipher ^ key 就恢复原数据。
  • 交换两个变量,不需要临时变量。

交换变量:看着炫酷,但也容易翻车

x = x ^ y;
y = x ^ y;
x = x ^ y;

每一步都在利用“两次异或复原”的性质。但如果你写的时候不小心让 xy 指向同一个变量,三重异或直接归零。所以这种写法更多是智力题级别的乐趣,真正在公司代码里请老老实实用临时变量——可读性比炫技值钱得多。


取反:反转镜里的二进制

~ 更进一步,它把一个数的每一个二进制位都翻转:0 变 1,1 变 0。像把黑白照片的底片拿出来看,黑成白,白成黑。

~6 的结果是多少?直觉上是“最后几位翻一下”,但真实现出来是 -7

为什么?因为 int 通常占 32 位。6 在内存里是前面一堆 0 再加 110,取反之后所有高位全都变成 1,符号位也变成 1,于是一个正数瞬间变成负数。补码形式下,1111...1001 表示的就是 -7。

有个速算公式:~x = -x - 1。验证一下,~6 = -7~0 = -1,没错。

新手最容易犯的错就是把 ~6 当成“最后三位取反”来算,以为结果是 9。那是在只有三位的世界里才成立,现实世界的 int 有 32 位,所有位一票全翻,结果当然要往负数方向跑。


一道题看清优先级有多坑

来看一个具体题目:已知 int a = 5, b = 3,问 (a ^ b) ^ a 的值是多少?

这题直接考察的是异或的自反性:(a ^ b) ^ a = b,所以答案是 3。

如果按顺序算一遍:5 ^ 3 = 6(二进制 0110),然后 6 ^ 5 = 3(二进制 0011),很顺。

但如果有人写成了 a ^ b ^ a 且不加大括号,那就要小心了——因为异或的优先级比 ==!= 还要低。比如:

if (a ^ b == 0)   // 实际先算 b == 0,再算 a ^ (b==0)

这已经不是“题意”问题了,而是整个表达式含义都被篡改。所以涉及多个运算符混合时,别吝啬括号。


再看一道和类型相关的取反题

有这样一个判断:unsigned char x = 0,那么 ~x 的结果是 255。

这个结论在某些语境下是对的。如果直接输出 ~x 的值,你可能会看到 -1,因为 C++ 里的 ~ 会对 unsigned char 先做整型提升,变成 32 位的 int,0 提升后是 0x00000000,取反得到 0xFFFFFFFF,也就是 -1。但如果你把这个结果赋值给 unsigned char,它只会保留低 8 位,0xFFFFFFFF 截断成 0xFF,也就是 255。

所以这道题的精髓在于:类型决定你看到的结果。你以为是对 8 位取反,实际上 C++ 先悄悄把它拉成了 32 位再取反,最后落到窄类型时再把多余的高位扔掉。

用代码验证一下:

unsigned char x = 0;
unsigned char y = ~x;   // y 是 255

如果你不看类型,直接 cout << (~x),大概率会看到 -1,然后就开始怀疑人生。理解整型提升和截断规则后,这就不再是玄学,而是明明白白的机制。


深入理解:为什么异或能成为加密的基础?

反观异或的运算表,它本质上是在做“比较差异”的运算。这种运算天然适合做数据扰动:把原始比特和密钥比特混合,相同位归零,不同位置一。攻击者看不到原始数据,却可以用密钥再次混合还原。更重要的是,异或运算没有进位,所以速度极快,在许多嵌入式芯片上几乎和加法一样廉价。

在实际应用中,它还能用来做标记位的动态翻转:

flags ^= (1 << 3);   // 把第3位开关取反

不用读回原值,不用判断,一条语句完成状态切换。这就是位运算的爽感——你不是在操作抽象的数字,而是在直接拨弄内存里的物理开关。


新手最常吃的几个亏

  1. ^|| 混在一起a ^ b 返回的是整数,不是布尔值。想判断“不相等”就用 a != b,不要用 a ^ b 的数值真假。
  2. 以为 ~ 只是“逐位翻转”这么简单。翻转完之后符号位也变了,结果是负数,要接受这个事实。
  3. 忽略整型提升。对 charshort 做位运算时候,结果可能和你预期的位数对不上,记得看类型。
  4. 异或交换变量时自我赋值。这是经典的坑,面试时可以提一句“不推荐”,比硬写一个 bug 要体面得多。

学完这两个运算符,下一步往哪走?

建议按这个顺序继续深入:

  • 按位与 & 和按位或 |,和异或是同一个家族,规则不同但用法相通。
  • 移位 <<>>,用位移实现乘法除法,配合掩码可以有效地设置和清除位。
  • 补码表示法,理解负数在内存里的真实形态,才能真正吃透取反的结果。
  • 位掩码(bitmask),在系统编程、嵌入式控制中无处不在。

如果你想挑战一下实战题目,可以去找“数组中只出现一次的数字”这类经典问题。它考察的正是 x ^ x = 0 这条性质,把重复的数字全部抵消掉,剩下唯一没有配对的数。所有位运算技巧都像是手里多了几把不同形状的螺丝刀,平时用不上,遇到对的问题时,一下就能拧得很顺手。


关于作者

我是赵老师,持有 NOI 信息学奥赛教练证书,拥有 15 年以上的软件开发经验,从事信息学少儿编程教学已有 8 年时间。

这些年累计帮助 多名 学生通过编程特长升入自己心仪的目标学校。

如果你在编程学习上有任何疑问,欢迎联系我:18620372957(微信同号)

这篇文章对你有帮助吗?

成为第一个评价的人

想系统学习这个知识点?查看完整知识点 →