从移位到异或:彻底搞懂二进制位运算的工程实战

发布时间:2026/10/11 12:11:32
从移位到异或:彻底搞懂二进制位运算的工程实战 我最近在复盘代码的时候就发现个很有意思的现象很多写了三五年业务代码的人遇到二进制做掩码、标志位拼接、数据校验这类需求时第一反应永远是% 2、拆数组、写循环很少有人能顺手丢出一句a ^ b或者1 n。一问为什么大家都说“移位和异或我看过但不知道什么时候用也不敢用”。这篇就把这两件事彻底讲透。会覆盖二进制视角下的移位左移右移、算术移位和逻辑移位的区别、异或的四条数学性质以及把它们组合起来能干的几件正经事。最后再附上我在不同语言里实测踩过的坑。适合刚接触位运算的人也适合想把手上的代码再抠快一点的老开发看完你能知道一段逻辑用还是* 2、用^还是各自付出的代价是什么。1. 先换一个视角整数在内存里就是一排开关很多人学移位和异或觉得抽象根本原因是习惯了十进制视角。你在纸上写27 45脑子里是竖式加法但写27 ^ 45再用十进制的思路去套当然怎么都想不明白。所以第一步不是记公式而是把“数字”这个概念替换成“位模式”。一个int在内存里不是给你看的一个数而是若干个二进制位排列成一排。比如 8 位的场景下25是00011001。你可以把这 8 个位想象成 8 个独立开关某个位置上是 0代表这个开关关闭是 1代表这个开关打开。整数的数值大小只是这排开关按权重累加出来的结果。权重怎么算呢从左往右每一位的权重是2^(位数-1)到2^0。最右边那位叫“最低有效位”权重是 1最左边那位在无符号数里叫“最高有效位”权重最大。所以00011001从右往左读第 0 位是 1第 3 位是 1第 4 位是 1加起来就是2^0 2^3 2^4 1 8 16 25。一旦接受这个设定移位就很好懂所谓左移就是把整排开关整体往高位方向推一格所谓右移就是整体往低位方向推一格。而异或^就更直接它做的事是把两个数对应的每一位拿出来相同出 0不同出 1按位独立处理。5 ^ 3为什么等于 6因为101 ^ 011 110。逐位算一位都不需要借位和进位这就是它和加法的本质区别。2. 移位运算左移、右移以及“丢掉什么、补进什么”2.1 左移的本质低位补 0高位溢出即丢失左移操作a n意思是把a的二进制位往高位移动 n 位空出来的低 n 位全部补 0从高位移出去的位直接丢弃。用生活化的比喻就像一条传送带往左推左边掉下桌的零件不要了右边空出来的位置放上默认的空箱子。举个例子。在 8 位环境里十进制 3 00000011 3 1 00000110 6 3 2 00001100 12 3 3 00011000 24肉眼观察一下规律3 1是 63 2是 123 3是 24全都是乘 2 的对应次方。所以有句口诀叫“左移一位等于乘 2左移 n 位等于乘 2 的 n 次方”。但这句口诀只在一定范围内成立。因为位数有限当一个数已经很大高位里原本有 1左移后这个 1 被推出去就会发生溢出。比如 8 位无符号数里128 1的结果不是 256十六进制的0x80左移一位变成0x00直接归零。这就是为什么处理标志位、状态掩码时要特别小心不要把有效的 1 推出边界。有符号数更微妙。先记住一件事在二进制里负数通常用补码表示。-1在 8 位里头是11111111-1 1得到11111110换算过来是-2。你说它是不是也等于乘 2是。-1 * 2 -2没毛病。但如果左移一位的数是64也就是01000000左移后变成10000000在无符号语义下这是 128在有符号语义下这是 -128。同一个位模式十进制解释完全不同。这个现象必须根植脑海左移只负责改变位的位置不负责保证数值结果按照十进制直觉成立。2.2 右移分两派算术右移和逻辑右移右移就没左移那么单纯了因为涉及“最左边补什么”。逻辑右移最高位无论原来是什么空出来的高位一律补 0。这适合无符号数。算术右移空出来的高位补符号位。原数最高位是 0 就补 0是 1 就补 1。这适合有符号数因为要保留负号。一个具体的例子最能说明问题。8 位场景下-8的二进制是11111000算术右移 1 位11111100 -4 逻辑右移 1 位01111100 124同一个-8同样右移一位两种规则天差地别。所以在绝大多数编程语言里int n对有符号类型默认走算术右移uint32_t n走逻辑右移。而在 Java 里额外提供了一个强制做逻辑右移不管类型有没有符号。C 和 C 则没有独立的符号全靠类型本身决定。这个区别不搞清楚线上排查问题会非常痛苦因为看起来一模一样的表达式在不同平台上结果可能完全不同。右移和除法的关系也要拎清。数学上右移 n 位等于整除2^n但更准确地说是向下取整的除法或者说带符号的除法。-3 1按算术右移结果是-2但-3 / 2在普通整数除法里通常是-1向零取整或-2向负无穷取整取决于语言。所以“右移就是除以 2”只在无符号数和正数里完全成立负数场景可能少一截。2.3 移位与乘除法的性能差到底还重要吗老早以前编译器很笨写x * 8真的可能生成一条乘法指令时钟周期比移位长得多。于是很多老一辈程序员的习惯是见* 2就改 1见/ 2就改 1。现在这个习惯还重要吗现代编译器的优化器非常成熟遇到x * 8、x / 8只要x是无符号整数或者编译时能确定无副作用编译器会自动生成移位指令。所以从纯 CPU 指令数看写* 8和 3没有差别。真正需要你手动写移位的场景不是因为乘法指令慢而是因为你要用移位去拼位模式、设置标志、分配内存对齐这些场景你需要的不是“数学上的乘”而是“结构上的移动”。比如在文件格式里写一个 BNF 字段需要把两个 4 位数字拼成一个字节那么(high 4) | low这种写法语义就是移位和拼接跟乘法一毛钱关系都没有。3. 异或二进制世界里非常关键的一把可逆钥匙3.1 异或的四条数学性质以及它们能推出的结论异或的运算规则按位看只有四种情况0 ^ 0 0、0 ^ 1 1、1 ^ 0 1、1 ^ 1 0。翻译成大白话就是两个位一样结果是 0两个位不一样结果是 1。没错异或可以当“二进制比较器”用但它最有价值的特性来自下面几条整体性质a ^ 0 a任何数和 0 异或等于自己因为每个位和 0 异或之后不变。a ^ a 0任何数和自身异或每一位都相同全部归 0。交换律a ^ b b ^ a。结合律(a ^ b) ^ c a ^ (b ^ c)。这三条加一条 0 的特性能推出两个特别重要的结论。第一异或运算可逆。所谓“可逆”是指如果c a ^ b那么a c ^ b、b c ^ a。这就像做了一次二进制层面的“加减密”用同一个密钥对数据异或一次再用同一个密钥异或一次数据就原样恢复。第二偶数次异或同一个值会互相抵消。这个性质用来做偶数元素成对消除在数组找单独元素时堪称降维打击。3.2 零开销交换两个数真的能这么用吗网上流传最广的异或应用是这个三段式// 交换 a 和 b不引入临时变量 a a ^ b; b a ^ b; a a ^ b;推导也不难。第一步之后a存着a_old ^ b_old。第二步b (a_old ^ b_old) ^ b_old a_old。第三步a (a_old ^ b_old) ^ a_old b_old。整个过程没有动用额外存储所以有些人吹它是“零开销交换”。但我必须说句泼冷水的话在现代 CPU 和编译器面前这个技巧几乎没有任何性能优势寄存器充足的时候三条mov指令也能完成交换而且更直观。这个三连异或更大的价值是帮助你理解异或性质而不是用于核心业务代码。因为当a和b指向同一块内存时比如同一个数组元素和自身交换第一步a ^ b会把那个内存位置直接清零后面第二步第三步拿到的都是 0数据直接丢失。这种行为在指针编程场景下是经典陷阱不要为了炫技把线上数据搭进去。3.3 状态翻转用异或切换开关比条件判断干净异或在实际代码里一个非常实用的用途是“切换位”。假设你有一个状态位想把它从 0 变 1、从 1 变 0常规写法是if (flag 0) { flag 1; } else { flag 0; }有了异或之后一句flag ^ 1搞定。因为 0 和 1 异或得到 11 和 1 异或得到 0。这在高频循环、嵌入式 GPIO 翻转、动画帧标志位切换里尤其好用少两次分支预测代码也更紧凑。更进一步如果要对多个位同时翻转就构造一个“翻转掩码”。比如我想把一个字节的低 4 位全部取反而高 4 位保持不变表达式是data ^ 0x0F。为什么能用异或因为异或每一位独立工作掩码对应位是 1就翻转原始位掩码对应位是 0就保持原始位。你不需要先读出来再判断再写回去一次异或全部完成。4. 移位和异或组合起来能干几件正经事4.1 构造掩码1 n、(1 n) - 1、~((1 n) - 1)掩码就是一段特殊的二进制模板用来和原始数据做按位与、按位或、按位异或从而提取或修改特定位。位移在这里是构造掩码的主力工具。1 n得到一个只有第 n 位为 1 的数。比如1 3是00001000。用于判断某一位是否为 1(value (1 3)) ! 0就是“value 的第 3 位是不是 1”。(1 n) - 1得到低 n 位全为 1 的数。比如(1 4) - 1 00001111。用于提取低 n 位value ((1 4) - 1)。~((1 n) - 1)得到除低 n 位外全为 1 的数。用于把低 n 位清零value ~((1 4) - 1)。这三件事是做网络协议、嵌入式寄存器配置、权限系统位图时最基础的操作。比如一个 8 位配置寄存器bit0 到 bit2 表示模式bit3 表示开关bit4 到 bit7 保留那你读写模式就只需要(register 0) 0x07提取然后register (register ~0x07) | newMode写回。4.2 用异或找“出现奇数次的数”逻辑很干净一个经典面试题一个数组里只有一个数出现奇数次其余数都出现偶数次找出这个数。常规做法是哈希表统计但用异或只要一行let result 0; for (let num of nums) { result ^ num; } // 最终 result 就是那个出现奇数次的数原理就是前面说的偶数次同值抵消。所有出现偶数次的数两两异或全部归零唯一剩下那个只出现一次的数与 0 异或得到它本身。这个过程不需要额外数组不需要比较时间 O(n)、空间 O(1)。你要是没理解异或的性质第一次看到这种解法会觉得像魔法理解之后就会发现它其实是把“两两抵消”的思想用数学方式编码了。4.3 简单加密与 CRC 校验异或为什么总出现在底层异或可逆这个特性让它天然适合做对称加密的底层算子。举个最简单的例子cipher plain ^ key解密时plain cipher ^ key。同一个 key 两次异或回原值密钥不需要保存两份。虽然这种简单异或在现代密码学里只能算玩具级别但它在很多轻量场景仍然真实存在比如某些传输协议的混淆、非安全场景下的数据掩藏、单片机和上位机之间的私有协议校验。再往外延伸一点CRC 校验算法里那些看起来天书一样的多项式运算底层核心也是移位和异或的组合。CRC 把数据当作一个长位串按多项式做无进位的二进制除法那里面的“无进位减法”本质就是异或。你去看任何 CRC32 的软件实现循环体里全是^和。理解了移位和异或你才能真正读懂这些库函数在干什么而不是只会调用。4.4 用一个实际例子串起来位标志权限系统光讲原理不够我拿一个权限系统的小例子把移位和异或串起来。假设有四种权限读、写、执行、删除分别用 bit0、bit1、bit2、bit3 表示。定义常量const READ 1 0; const WRITE 1 1; const EXEC 1 2; const DEL 1 3;某个用户拥有读和写权限perm READ | WRITE;结果是00000011。判断是否有执行权限(perm EXEC) ! 0。给用户追加执行权限perm | EXEC;撤销用户的写权限perm ~WRITE;让用户对某项权限做翻转perm ^ DEL;这段代码用最少的存储表达了多个开关状态而且所有判断都是几条指令级别。换一组场景网络封包头、数据库权位标记、嵌入式寄存器状态、甚至游戏里的 buff 列表只要开关数量不超过整数位数都可以用同一套写法。等以后真遇到“一个字段表示 32 个开关”的需求你就知道位运算为什么比数组省钱了。5. 最容易踩的坑我一个个试给你看5.1 移位数超过类型位宽是未定义行为在 C 语言里a 32当a是 32 位整数时行为是未定义编译器可能给出你完全想不到的结果。我实测过有的机器上1 32等于 1相当于移位次数被截断成 0有的机器上直接是乱七八糟的值。原因是底层 CPU 指令只取移位次数的低 5 位或低 6 位超过部分被悄悄丢弃这个行为在不同平台可能不同。真正的工程代码里务必先对移位数做边界判断n % 位宽或者提前if排除。Java 的选择更保守一点Java 对移位距离做了取模处理。int是 32 位移位距离取低 5 位也就是1 32等于1 0结果等于 1。这虽然规避了未定义行为但也很容易让人误以为左移 32 位会得到 0。写业务代码的时候别指望这种“安全”兜底自己把范围判断写清楚。5.2 有符号数右移符号位可能让数值变负数这是线上排查里最经典的坑。在 C/C 和 Java 等语言里对负号整数做默认算术右移符号位不断扩展。你以为是在“除以 2 向 0 取整”实际上它朝向负无穷取整。-5 1的结果是-3而不是-2这个误差在循环边界和数组索引计算时很容易引出隐蔽 bug。如果你确实需要逻辑右移在 Java 里要用在 C 里除了把变量类型改成无符号数几乎没有直接的运算符。所以只要一个二进制位串将来要被“解释为数值”就不要随手右移最好明确类型和移位规则。5.3 异或运算符优先级很低括号必须加C、Java、JavaScript 里^的优先级都低于和!。举个例子int a 5; int b 3; if (a ^ b 6) { ... }你以为是先算a ^ b得到 6再和 6 比较成立。实际上优先级高于^表达式被解析成a ^ (b 6)也就是5 ^ 0 5。这个 bug 非常隐蔽因为表达式的值不会触发编译器警告。我的经验是只要表达式里除了位运算还有比较、逻辑运算一律给位运算部分加括号不要省那两下键盘。5.4 Python 里的无限位宽是个双刃剑Python 的整数是任意精度移位和异或同样适用但表现和 C 不一样。一个重要区别是Python 的右移对有符号数一律是算术右移并且因为无限精度负数的二进制表示永远带无限个前缀 1。比如-1 10结果还是-1因为不管移多少位无限延伸的符号位 1 都在那里。另一个容易惊讶的点是~5结果是-6不是像有些人想的那样“5 取反得到某位串”。因为 Python 的~是从无限位模式取反-5 减 1。处理协议、位图时如果从 Python 转 C 语言这两处最容易对不上。6. 常见问题速查表直接在项目里对号入座场景推荐写法需要注意的坑让整数乘 2 的 n 次方x n高位可能溢出检查结果范围让整数除 2 的 n 次方x n有符号负数右移是算术移位结果可能向负无穷取整判断 value 第 n 位是否 1(value n) 1n 不要大于等于位宽提取低 n 位value ((1 n) - 1)注意(1 n)的类型可能溢出把低 n 位清零value ~((1 n) - 1)确认~在目标语言里不会因为符号扩展出问题设第 n 位为 1value | (1 n)多个位同时设置用或运算设第 n 位为 0value ~(1 n)掩码取反后再与翻转第 n 位value ^ (1 n)不影响其他位天然可逆两个相同的数异或后续结果互相抵消适合找唯一奇数次数交换两个数不临时变量异或三连两个变量指向同一内存时会清 0谨慎简单对称加密plain ^ key只能用于非高安全场景注意密钥管理7. 按我自己的使用习惯最后分享三点经验第一新手练移位和异或最适合的场地是 LeetCode 里所有带“位”“幂次”“出现次数”这几个关键词的题每道题做完都把位运算版解法重写一遍。不要背题解自己拿笔把每个位的变化画出来画上十几道感觉自然就来了。第二在实际项目里我有个检查习惯凡是看到* 2、/ 2、% 2都会下意识想一想能不能用位运算替代但只在做标志位、协议解析、性能敏感循环时才真正替换。普通业务逻辑里保持可读性比节省两个时钟周期重要得多。位运算写出来确实很帅但三个月后维护代码的人可能是你自己别让帅变成负担。第三一次线下调试经历给我留下的印象非常深某跨平台系统的内存缓冲池莫名出现首字节被清零的问题排查了很久最后定位到就是有同事写了a a ^ b; b a ^ b; a a ^ b;来交换指针结果两个指针恰好指向同一块内存的首地址。从那以后我对异或三连交换就只剩一个态度欣赏它的数学美感但绝不在指针和引用场景用它。类似地所有涉及移位和异或的写法我都会在关键行加一行注释写清楚“这里在做什么位操作、期望什么结果”避免后来者看到一坨二进制魔法抓狂。工具是拿来解决问题的不是拿来炫技的。把移位和异或真正用到手你不但能写出更快的代码更重要的是能看懂底层系统里那些“看起来不像人话”的位运算逻辑。多了这层视角再回头看网络协议头、文件格式、驱动寄存器很多天书会一下子豁然开朗。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询