
写代码这些年遇到“判断某个开关是不是开着”这种需求太常见了。一个整数变量里可能同时存着好几个状态要判断其中第几位是1、第几位是0或者解析一段二进制协议要从某个字节的指定位里抠出版本号、标志位。说句实话位运算在业务代码里出现频率不算高但一旦遇到会写的人一眼看穿不会写的人可能折腾半天。今天要聊的就是位运算系列里的经典动作——获取整数n第x位的值也就是常说的 test bit、get bit。这个操作能做什么简单说就是从一个整数的二进制表示里单独挑出第 x 个二进制位看清它装的是 0 还是 1。它适合刚接触位运算的新手打基础也适合有经验的读者把边界情况再捋一遍。我会从公式原理、多语言实现、实际应用场景、常见坑位几个角度展开争取把这件事讲透。1. 位运算取位的真实需求从开关状态到协议解析1.1 为什么会有人想把一堆开关塞进一个整数假设你正在开发一个小型设备管理工具需要记录每个设备的八种状态在线、启用、告警、锁定、待重启、固件更新中、电池低、配置异常。最直觉的做法是定义八个布尔变量但如果你要和远端通信、把状态一次性写入数据库或者用 JSON 序列化八个布尔字段就得来回拆包组装。更简洁的方案是定义一个整数用它的 8 个二进制位分别表示这八种状态。低 0 位表示在线低 1 位表示启用低 2 位表示告警……这样一来一个变量就能装下所有开关更新时只需要一次赋值传输时也只需要一个字段。这种把多个开关编码进单个整数的做法在嵌入式开发、网络协议、权限系统里非常普遍。寄存器里某个位表示外设是否使能TCP 头里某个位表示 SYN、ACK 标志Linux 文件权限里 rwx 直接就是三组位。在这些场景里最基础、最频繁的操作就是“读取某一个位的值”。你不能直接拿整个整数当布尔值用因为整数同时代表了多个开关你必须先把目标位单独拎出来。这就是为什么“获取整数 n 第 x 位的值”看起来像教科书习题实际上却是每天都在用的基础能力。1.2 哪些场景离不开“取某一位”我把日常开发里用到取位操作的典型场景列一下读者可以对照着感受一下自己是不是也遇过权限检测一个权限整数里bit 0 表示可读、bit 1 表示可写、bit 2 表示可执行判断当前用户能否写文件就是获取第 1 位的值。状态开关设备或服务用一个状态字上报多个状态比如某位表示网络是否连通、某位表示磁盘是否只读前端展示时逐位读取。协议解析解析二进制协议头比如从第一个字节里取高 4 位作为版本号直接(byte 4) 0xF这本质上就是取“第 4 到第 7 位”。颜色编码还原RGB565 格式的像素用 16 位整数存储三原色还原红、绿、蓝分量时需要分别取高位、中位、低位。布隆过滤器与位图判断一个元素是否可能已存在就是查看位数组里的某一位是否为 1。这些场景里取位操作都是最内核的步骤后面每一步判断都依赖它。所以把n x 1彻底弄懂是能在不少领域直接省时间的。2. 动手前必须理清的三件事位编号、右移、按位与2.1 位编号从0开始还是从1开始很多人第一次接触取位操作就被编号问题绕晕。我需要先把这件事说清楚在绝大多数编程语言、CPU 指令、网络协议文档里位的编号默认从 0 开始最低位最右边那位称为 bit 0。比如整数0b1101从低到高分别是 bit 01、bit 10、bit 21、bit 31。说“获取第 2 位”指的是从右边数第 3 个位置即权值为 4 的那一位它是 1。小部分是例外比如某些硬件芯片手册会用“第 1 位”表示最低位或者从 1 开始编号。这和编程习惯冲突时调试几小时找不到问题也不奇怪。我的建议是在代码里统一约定位编号从 0 开始注释里写得清清楚楚。遇到协议文档说“bit 1”时先确定文档的起始是 0 还是 1再对应到代码里的偏移量。千万别想当然。这个细节看起来简单却是取位错误的最常见源头之一。2.2 右移操作把目标位搬到最低位在需要获取第 x 位时最直观的思路是把这一位“挪”到最容易观察的位置——最低位bit 0。右移运算就是干这个的。n x表示把 n 的所有二进制位整体向右移动 x 个位置原来的第 x 位会移动到第 0 位原来的第 x1 位移动到第 1 位依此类推。右移之后我们关心的目标位就到了最低位可以直接读取。需要注意右移对无符号整数和有符号整数的处理方式不同。无符号整数右移时高位补 0这叫逻辑右移有符号负数右移时高位通常补的是符号位1这叫算术右移。现在先不深入后面第 4 章我会专门讲这个坑。这里先记住n x能让目标位到达最低位为下一步“只留下最低位”做好准备。2.3 按位与精确屏蔽其他位右移之后最低位是我们感兴趣的位但更高的位置还残留着其他位的信息不能直接当作结果。需要用一个掩码把其他位清零只保留最低位。这个掩码就是数字1二进制是0b1。按位与运算 1的效果是结果的每一位只有两个操作数对应位都为 1 时才为 1否则为 0。因为我们与的是1而1只有最低位是 1其他位都是 0所以任何数与1做与运算高位置全部被清零只剩下最低位原封不动。这是个非常经典的手法可以类比成过滤网一个很细的筛子只允许最底下的那一个颗粒通过上面的全被拦住。(n x) 1的结果只有两种可能0 或 1。如果第 x 位是 0结果为 0如果第 x 位是 1结果为 1。得到这个 0 或 1 之后再配合if判断、布尔转换或者查表就能完成各种业务逻辑。3. 核心公式 n x 1 拆解一步步算出第x位的值3.1 拿一个具体数字手推一遍公式本身极短就三个符号(n x) 1但隐藏了不少细节。我用一个具体例子完整走一遍。假设n 13二进制是0b1101共 4 位有意义。我想获取第 2 位从 0 开始的值也就是0b1101中从右往左数第三位这一位是 1。用公式就是(13 2) 1。第一步右移 2 位13 2。13 除以 2 的 2 次方4得到 3二进制就是0b11。从位排列的角度看原来第 2 位1移动到了第 0 位原第 3 位1移动到了第 1 位。第二步按位与0b11 0b1结果是0b1也就是整数 1。所以第 2 位的值是 1。再看一个更清晰的例子如果n 10二进制0b1010获取第 1 位(10 1) 1 0b101 0b1 1第 1 位是 1获取第 0 位(10 0) 1 0b1010 0b1 0第 0 位是 0。整个过程就是“移位 与 1”没有任何多余的判断几个 CPU 周期就能搞定。如果目标是判断这一位是否为 1也可以直接if ((n x) 1)代码简洁意思明确。3.2 两种等价写法怎么选除了(n x) 1江湖上还有一种常见写法(n (1 x)) ! 0。这两种方式等价但风格不同。(n (1 x)) ! 0的思路是先把1左移 x 位生成了一个只在第 x 位为 1 的掩码。比如 x2 时1 2 0b100。然后 n 与这个掩码做按位与如果第 x 位为 1结果就是0b100非零判断为真如果第 x 位为 0结果就是 0判断为假。从而完成“判断第 x 位是否为 1”。相比之下(n x) 1的结果是一个干净的 0 或 1可以直接作为数值使用(n (1 x)) ! 0的结果是布尔值或非零值更适合直接放到条件里。如果后续还要把这第四位当成数值参与计算我推荐第一种写法如果只是做条件判断第二种也很自然。编译器优化后两种写法的性能几乎没有差异选哪个更多是代码可读性偏好。我个人习惯是只要后续逻辑需要 0/1 数值一律写(n x) 1单纯判断就写if (n (1 x))因为不需要额外的! 0也比较直白。3.3 顺便提一句运算符优先级(n x) 1我建议始终带上括号。C 系语言里右移的优先级高于按位与所以n x 1其实等价于(n x) 1不会出错。但问题是人类肉眼扫代码的时候没有括号很容易一恍惚就理解错而且不同语言、不同编译器对连续位运算的优先级记忆很容易混淆。我的经验是位运算代码本身已经够“密”了再省括号就是给自己埋雷。多写两个括号能让半年后的自己少掉点头发。在团队协作时这个习惯也能减少 Code Review 时不必要的口舌。4. 多语言实操代码怎么写边界怎么兜4.1 五种常用语言的实现取某一位的代码在不同语言里几乎同构只是因为类型系统不同细节略有差异。我先给出五个常见语言的版本。C/C#include cstdint // 返回 0 或 1 uint32_t get_bit(uint32_t n, int x) { return (n x) 1u; }Javapublic static int getBit(int n, int x) { return (n x) 1; }Pythondef get_bit(n: int, x: int) - int: return (n x) 1Gofunc GetBit(n uint, x uint) uint { return (n x) 1 }JavaScriptfunction getBit(n, x) { return (n x) 1; }这里需要注意Java 和 JavaScript 如果对负数使用算术右移负数符号位会被无限复制导致高位全是 1但因为我们最后会 1结果仍然是正确的第 x 位值。可问题是Java 的在做无符号逻辑右移时应该用用更稳妥尤其当这个整数来自某段二进制协议时我们更多是从“无符号视角”去解读它的位。C/C 建议直接把类型声名为无符号类型从源头规避符号位扩散。Go 语言也建议入参用无符号类型uint或uint32。这些细微差异不处理在负数、高位场景下就会踩坑。4.2 有符号与无符号负数、右移与结果的关系有符号整数在计算机里用补码表示。负数的最高位为 1例如-2在 32 位下是0xFFFFFFFE。如果用右移操作-2 1在大多数编译器和 JVM 实现里因为它是算术右移结果会是0xFFFFFFFF也就是 -1高位全是 1。这会导致什么现象比如我要取-2的第 3 位二进制是1111...1110第 3 位是 1用(n 3) 1照样能算出 1没问题。但如果我要取一个“本不该存在”的更高位比如第 31 位结果也是 1因为负数高位全是符号扩展的 1。有些场景这算是预期行为有些场景是意外。最稳妥的做法是先做无符号化再取位。在 C 里直接把类型转成uint32_t在 Java 里用在 Python 里由于整数是任意精度的负数右移本质上也是算术右移解决思路是先把负数的补码截断到固定位数例如模拟 32 位无符号数n 0xFFFFFFFF然后再(n x) 1。如果只是判断一个普通 Python 整数的某个位直接(n x) 1就很好用只有在做协议解析、想按固定位宽解读负数时才需要先截断。理解了这一点很多看似“玄学”的结果就都能解释通了。4.3 移位量越界不同语言的“奇怪”表现这是取位操作里最容易翻车的边界条件x 太大甚至超过这个整数本身的位宽。不同语言的表现完全不一样。C/C 里如果 x 大于等于类型位宽右移是未定义行为编译器可能给你任何结果程序可能崩也可能返回垃圾值。写代码时必须先判断x bit_width或者用取模等方式限制。Java 里移位运算的距离会被隐式地跟 31int或 63long取模也就是说(n 32)实际上相当于(n 0)结果是 n 本身。想在 Java 里获取第 32 位需要通过long或BigInteger或者干脆把 n 当成无符号 64 位用配合。JavaScript 的位运算更特殊所有按位运算都会先把数字转成 32 位有符号整数x也要先跟 31 取模所以 32 位以上的位操作直接失效。Python 则没有 32 位或者 64 位的限制整数无限大右移再多位也不会越界但负数的符号扩展会一直持续。所以网上抄取位代码时一定要看清楚语言。我建议定义一个统一的辅助函数在函数入口处校验 x 的合法范围或者在注释里写明“x 必须小于等于 bit_width - 1”。如果你处理的是固定协议最好把“位宽”也作为一个参数传进来让调用方清楚这个位操作是在几位的整数上进行的。4.4 性能代价与优化建议取位操作本身非常廉价。在 x86 体系上编译器往往会把它优化成btbit test指令专门用来测试某一位或者用shrand两条指令完成。不管哪种都是纳秒级别。就算在一秒内执行几百万次也不会成为瓶颈。反而真正影响性能的是你在这个取位操作前后做的事情比如循环遍历 32 个位每个位都调一次函数、做一次分支判断这种写法会让分支预测压力变大。如果确实要遍历所有位可以考虑查表法预先把 0 到 255 每个数字每一位做一张位表然后按字节查询能减少移位和循环指令数量。但在绝大多数业务场景下均摊性能差异不大优先保证代码可读性更实际。5. 应用实战状态标志位、RGB565与位字段解析5.1 一个整数存8个开关权限与配置位举一个最近我在某个项目里写的例子。系统给每个后台用户一个权限整数字段permissions按位划分bit 0 可以查看列表bit 1 可以创建资源bit 2 可以编辑资源bit 3 可以删除资源bit 4 可以管理成员。要检查当前用户能不能删除资源代码就是(permissions 3) 1。如果要给用户一次性赋予查看和编辑权限可以写成permissions | (1 0) | (1 2)要撤销删除权限写成permissions ~(1 3)。这里有个小经验判断权限的代码我一定会封装成常量。比如const int PERM_DELETE 3;然后用getBit(permissions, PERM_DELETE)而不是在代码里裸写数字 3。否则三个月后回头读代码看到(permissions 3) 1你可能要想半天“3”是哪个业务含义。位运算能压缩信息但也会带来可读性损失用命名补偿是必须的。5.2 RGB565 颜色拆包高5位、中间6位、低5位RGB565 是一种常见于嵌入式屏幕和图片编码的颜色格式使用 16 位整数表示一个像素高 5 位是红色分量、中间 6 位是绿色分量、低 5 位是蓝色分量。要从一个uint16_t pixel里还原出三个颜色分量取位操作是最直接的工具uint16_t pixel 0xABCD; uint8_t r (pixel 11) 0x1F; // 高 5 位 uint8_t g (pixel 5) 0x3F; // 中间 6 位 uint8_t b pixel 0x1F; // 低 5 位注意这里取高 5 位不是只取 1 位而是取一段连续的位用的是更大的掩码。0x1F是二进制0b111110x3F是0b111111。这和取单一位的原理完全一致只是掩码从1变成了多个 1。如果你还没有写过类似代码可以试着把0xABCD展开成二进制然后按 RGB565 的分组规则重新组合一下会非常有画面感。这就是位运算最实用的一个应用数据打包和解包本质上全是移位、与、或。5.3 协议头解析与位字段读取取位操作在协议解析里更是家常便饭。比如某个自定义工控协议的消息头第一个字节高 4 位表示协议版本号低 4 位表示消息类型。读取版本号的代码是(headerByte 4) 0xF读取类型的代码是headerByte 0xF。再比如 TCP 首部里的 9 个标志位每一个都占用一个 bit解析时就是逐位判断。这些操作帮助开发者从一串字节流里迅速提取出零散的语义信息不需要引入复杂的结构体对齐或者解码框架。如果字段跨越多字节比如一个大端整数的高 16 位和低 16 位分布在不同的字节偏移上通常的做法是先按字节拼接成整型再用移位取位。我看到很多新手在这一步容易迷糊直接把字节数组里每个字节当成一个独立整数去取位导致位序判断混乱。建议先把整段数据拼接成一个无符号整型或者依次左移再取位会让边界清晰许多。5.4 布隆过滤器与位图判断布隆过滤器底层通常是一个位数组判断某个元素是否存在时先用若干哈希函数计算出数组下标然后检查这些下标对应的位是否为 1。这里的“检查某位是否为 1”就是(bits[idx / 8] (idx % 8)) 1。位图统计、磁盘块占用标记、内存页状态管理也广泛应用同一种操作。在这些大规模系统里取位操作是原子性的基础操作甚至会被封装成专门的位图库来处理并发访问。理解了单一位的读取再去读这些库的源码基本不会有障碍。6. 常见问题与排查技巧实录6.1 一张表看懂入门阶段最常踩的五个坑我把这几年在代码审查和讨论区里常见的取位问题整理成一张速查表方便读者遇到诡异现象时快速对照排查。症状常见原因解决方法结果和预期完全相反位编号从 1 开始实际代码从 0 开始统一用 bit 0 表示最低位协议文档先查起始约定负数取高位总是 1有符号数算术右移高位补 1先转无符号类型或用逻辑右移x 较大时结果不稳定C/C 移位量越界是未定义行为入参校验x bit_widthJava 里x32结果错误位移量被隐式取模32 变成 0改用 long 或进行位宽判断用n x判断某一位把 x 当成了位掩码应该用(n (1 x)) ! 0最后这一条值得多说一句。n x是把整数n和下标x直接做按位与和“取第 x 位”完全是两回事。如果x3它检查的是 n 的哪些位同时和二进制0b11都为 1这通常不是你想要的。6.2 调试技巧把二进制“摊开”看取位出错时最有效的调试手段就是打印二进制。把数值展开成 0 和 1 的序列问题一眼就能定位。C 里可以用std::bitsetPython 用bin(n)Java 有Integer.toBinaryStringGo 用fmt.Sprintf(%b, n)。如果你不想依赖语言内置功能也可以自己写一个简单的循环从高位到低位依次判断并输出(n i) 1。很多人在调试时喜欢直接用十进制相减、相乘很容易被补码和掩码绕晕。我建议优先输出二进制并且按 4 位一组分隔这样十六进制和二进制能直接对应。例如0xAB是1010 1011一眼能看出高 4 位是1010低 4 位是1011。排查位问题时这个习惯能帮你省掉至少一半的推理时间。6.3 一个很实用的自查顺序如果取位结果不对我会按照固定顺序排查。先看“我要的第 x 位”到底是从 0 还是从 1 编号再看“n 的类型”是有符号还是无符号然后看“x 的合法范围”是否越界最后检查我用的运算符是还是、是还是|。这四个环节里九成问题都能定位。假如排查完还是不对就用二进制输出对比预期值基本没有绕不过去的坑。7. 最后分享一些我的实操习惯与扩展想法7.1 我会把取值、置位、清除、翻转封装成一组工具函数“获取第 x 位的值”很少单独出现它通常和“置位”、“清除”、“翻转”一起用。我会把这些操作封装在一个工具模块里形成一组一致的 API避免每次写(n x) 1时还要想想和|的优先级。inline uint32_t getBit(uint32_t n, int x) { return (n x) 1u; } inline uint32_t setBit(uint32_t n, int x) { return n | (1u x); } inline uint32_t clearBit(uint32_t n, int x){ return n ~(1u x); } inline uint32_t toggleBit(uint32_t n, int x){ return n ^ (1u x); }有了这套工具函数业务代码会清楚很多。比如给用户加上“允许导出”的权限直接permissions setBit(permissions, PERM_EXPORT);检查是否有权限直接if (getBit(permissions, PERM_EXPORT) ! 0)。我认为这是从事嵌入式、协议开发的同学应该熟练掌握的小工具集。7.2 这个操作还能怎么扩展单个位的“取”理解透后可以很自然地扩展到“取一段连续位”也就是位域提取(n start) ((1 len) - 1)。这在视频编码、图片解码、压缩算法里会用得非常多。也可以扩展到批量操作用一个 32 位整数的每一位去并行处理 32 个布尔状态这就是所谓的 SIMD 思想的前身。我从一个简单取位公式出发逐步写到多语言实现和应用场景最后建议读者也试着思考一下“如果把一个整数看成 32 个布尔值的并行集合哪些算法可以因此简化”。这种思维方式比记住公式本身更值钱。回到个人体会我在实际项目里用到取位最多的往往不是炫技而是解析二进制格式时那种“原来一个字节里藏着这么多信息”的恍然大悟。写下这篇文章时我也顺便把常用的位工具函数重新整理了一遍。如果下一次再有人问我“怎么获取整数 n 第 x 位的值”我大概会直接把这篇文章丢过去然后补一句位运算这东西多用几次就再也忘不掉了。