ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

C语言按位与()详解:原理、六大应用场景与高频坑

C语言按位与()详解:原理、六大应用场景与高频坑 C语言快速通关系列做到第 83 期这次讲的是一个看起来很简单、实际上经常把人绕进去的运算符按位与。很多初学者能背出“两个 1 才是 1”但一遇到实际代码就分不清什么时候该用什么时候该用什么时候必须加括号什么时候结果会和自己想的不一样。这篇文章会把这些事情一次性讲清楚。我们从一个最小示例开始逐步拆解按位与的二进制运算过程然后给出 6 个典型使用场景判断奇偶、提取位段、清除特定位、判断 2 的幂、权限标志位、哈希桶索引。最后重点讲两个高频坑的优先级高于以及和的逻辑差异。如果你正在准备 C 语言期末考试、计算机等级考试或者刚开始接触嵌入式寄存器操作这篇文章可以直接收藏。读完之后你可以照着示例在自己的编译器里跑一遍遇到报错也能自己排查。1. 本讲核心速览项目说明本讲内容C 语言按位与运算符前置知识二进制转换、int/char 类型、printf 基本使用核心规则两个二进制位同时为 1结果才为 1常见用途判断奇偶、掩码提取、清除位、判断 2 的幂、权限标志、哈希索引需要掌握的写法(x mask)、(x n) 1、x (x - 1)最容易踩的坑优先级高于判断时必须加括号建议练习方式手算二进制后再跑代码验证适用方向C 语言笔试、嵌入式开发、底层驱动、图像处理、协议解析这一讲不涉及指针、结构体、链表这些复杂概念。只要你能看懂int、unsigned int、for循环和printf就能顺畅地学完。2. 核心概念按位与到底在算什么2.1 先把二进制摆出来计算机里的所有整数运行到 CPU 层面都是二进制。按位与的“位”就是指二进制位运算符是。运算规则只有四条1 1 11 0 00 1 00 0 0浓缩成一句话有 0 得 0全 1 才 1。我们用一个具体例子推演unsigned char a 0b11001100; unsigned char b 0b10101010; unsigned char c a b;把二进制位按位置对齐a 1 1 0 0 1 1 0 0 b 1 0 1 0 1 0 1 0 ----------------- c 1 0 0 0 1 0 0 0最高位两个都是 1结果是 1第二位一个 1 一个 0结果是 0后面依次类推。最后得到二进制10001000也就是十进制的 136。整个过程不涉及进位、不涉及借位每一位独立计算。这是按位与和加减法最大的不同之处。2.2 运算对象和结果类型按位与是双目运算符操作数必须是整型。char、short、int、long、unsigned系列都可以参与运算但浮点数不行。float f 3.14f; int x f 1; // 错误invalid operands to binary 如果你真的需要查看浮点数的二进制表示需要借助union或者memcpy把它转成无符号整型再配合位运算处理。这是另一个话题本讲先不展开。写代码时只要记住按位与的天然操作对象是整数。3. 最小可运行示例从代码里看懂结果先看一个可以直接编译运行的完整程序#include stdio.h int main(void) { unsigned int a 0xAC; // 二进制1010 1100 unsigned int b 0x5A; // 二进制0101 1010 unsigned int c a b; // 期望0000 1000即 0x08 printf(a 0x%02X\n, a); printf(b 0x%02X\n, b); printf(a b 0x%02X\n, c); return 0; }用 GCC 编译运行gcc -Wall -stdc11 main.c -o main ./main输出结果a 0xAC b 0x5A a b 0x08逐位核对一下0xAC 1010 1100 0x5A 0101 1010 0000 10000000 1000就是0x08和程序输出完全一致。这一步看似简单但它是后面所有位运算技巧的基础。如果你发现自己推导的结果和程序输出不一致说明二进制换算还没过关请回到 2.1 节重新手算一遍。调试位运算问题有个小习惯尽量用十六进制输出而不是十进制。printf(result 0x%08X\n, result);十六进制和二进制之间的转换非常直观一个十六进制字符对应四个二进制位。如果用十进制输出位模式会被掩盖很难发现问题。4.和长得像作用完全不同这是 C 语言初学者最容易被绕晕的地方。是按位与操作对象是整数的二进制位结果仍然是整数。是逻辑与操作对象是表达式的真假结果只有0或1而且它还有一个重要特性短路。对比点按位与逻辑与操作数整数按位计算任意标量表达式按真假判断结果整数可能是任意值只有 0 或 1是否短路否两侧都会求值是左侧为 0 时右侧不执行典型用途位掩码、提取位、清零、置位分支条件、多条件组合判断优先级低于高于^、|低于按位或 看一个典型场景int flags 0b100; // 假设第 3 位表示“可读” if (flags 4) { printf(可读\n); }这个代码能正常工作因为flags 4计算结果是4非零。if 条件判断只要非零就是真。但是注意flags 4的值是4不是1。如果你把这个表达式的结果直接赋值给一个布尔变量然后进行严格比较可能会产生困惑int result flags 4; // result 是 4所以在条件判断中表达的是“结果非零”不是“结果为 true”。这和返回严格0/1是有区别的。再来看短路的差异。这段代码是安全的int x 0; if (x ! 0 (10 / x) 2) { // 因为 x ! 0 为假右侧的 10 / x 不会执行 }但如果把写成if (x ! 0 (10 / x) 2) { // 右侧照样执行会触发除零错误 }没有短路机制左右两侧的表达式都会被求值。10 / 0是未定义行为程序可能崩溃。结论很简单逻辑判断用位操作才用。不要把两者混着写。5. 六大实际应用场景这一节是文章的重点。每个场景都会给出代码、二进制推演和注意事项。5.1 判断奇偶性一个整数的二进制最低位是0它就是偶数是1它就是奇数。用按位与可以直接判断#include stdio.h int main(void) { int n 2026; if (n 1) { printf(%d 是奇数\n, n); } else { printf(%d 是偶数\n, n); } return 0; }n 1只关心最低一位。2026 的二进制最低位是 0所以条件为假输出“偶数”。传统写法是n % 2 0。两种写法在编译器优化后性能基本没有差别但n 1的语义更直接只看最低位。在需要频繁判断奇偶的循环里很多老代码会这么写。注意负数场景int a -3; printf(%d\n, a % 2); // C99 之后通常是 -1 printf(%d\n, a 1); // 1如果你的目的是判断奇偶 1的结果更符合直觉负数也有奇偶性。5.2 提取特定位提取位是位运算最核心的应用之一。比如我们要从一个字节中取出低 4 位unsigned char val 0xD6; // 二进制1101 0110 unsigned char low4 val 0x0F; // 结果0000 0110即 0x060x0F的二进制是0000 1111和val按位与之后高于第 3 位的所有位都被清零低 4 位保留原值。如果要提取高 4 位先右移再掩码unsigned char high4 (val 4) 0x0F; // 结果0000 1101即 0x0D如果只想提取第 n 位标准写法是int bit (val n) 1;这个写法在寄存器操作、通信协议解析、位图处理中到处都是。可以把它们封装成宏#define GET_BIT(val, n) (((val) (n)) 1U) #define GET_BITS(val, high, low) \ (((val) (low)) ((1U ((high) - (low) 1)) - 1U))封装宏时必须把参数外层加括号否则容易在复杂表达式中触发优先级问题。5.3 清除特定位把某一位置 0同时不影响其他位是嵌入式开发中最常见的需求之一。假设要把一个寄存器字节的第 3 位清零unsigned char reg 0b11111111; reg ~(1U 3);推演过程1U 3 0000 1000 ~(1U 3) 1111 0111 reg ~(1U3) 1111 0111只有第 3 位被清零其他位保持不变。注意~的优先级高于所以reg ~0x08等价于reg reg (~0x08)。但是~0x08在 int 类型里是0xFFFFFFF7而不是0xF7。如果reg是unsigned char按位与之后高位会被截断结果没问题如果reg是 32 位宽度高位也会变成1结果可能不符合预期。所以清位操作之前一定要想清楚当前变量宽度。5.4 判断一个数是否是 2 的幂这是一个很经典的技巧。如果一个正整数是 2 的幂那么它的二进制表示里只有一个 1。此时x (x - 1)一定等于 0。比如8 1000 7 0111 8 7 0000再比如12 1100 11 1011 12 11 1000不为 0函数写法int is_power_of_two(unsigned int x) { return x ! 0 (x (x - 1)) 0; }这里必须先判断x ! 0因为0 -1的结果是 0如果不排除 0函数会把 0 也当成 2 的幂。这个技巧还衍生出一个经典算法统计二进制中 1 的个数。int count_ones(unsigned int x) { int count 0; while (x) { x (x - 1); count; } return count; }每次循环都会消掉最右边的一个 1循环次数就是二进制中 1 的个数。5.5 权限标志位当系统需要保存多个布尔开关状态时用位标志比用多个变量节省空间。比如文件权限#define PERM_READ (1U 0) // 0001 #define PERM_WRITE (1U 1) // 0010 #define PERM_EXEC (1U 2) // 0100检查是否有写权限unsigned int perm PERM_READ | PERM_EXEC; // perm 0101 if (perm PERM_WRITE) { printf(可写\n); } else { printf(不可写\n); }perm PERM_WRITE的结果是0000或0010非零即真。撤销某个权限perm ~PERM_EXEC;追加某个权限perm | PERM_READ;权限系统用位操作的好处是一个unsigned int最多能保存 32 个开关状态内存占用小判断也快。但写代码时必须注意检查权限的判断写法也就是下面要说的括号问题。5.6 颜色通道提取图像处理中一个像素常被存储为ARGB8888格式一个 32 位整数包含 A、R、G、B 四个通道每个通道 8 位。用按位与可以快速拆分unsigned int color 0xA1B2C3D4; unsigned char a (color 24) 0xFF; unsigned char r (color 16) 0xFF; unsigned char g (color 8) 0xFF; unsigned char b color 0xFF; printf(A%02X R%02X G%02X B%02X\n, a, r, g, b);输出AA1 RB2 GC3 BD4这里的 0xFF作用是把高 24 位清零只保留最低 8 位。这个套路在 LCD 驱动、图像裁剪、颜色转换代码里很常见。理解了一次以后遇到 RGB565、ARGB1555 之类的格式思路也一样先移位再掩码。5.7 哈希表桶索引当哈希表的桶数量是 2 的幂时hash (size - 1)可以替代hash % size。unsigned int hash 100; unsigned int bucket hash 15; // 等价于 hash % 16因为 16 的二进制是1000015 的二进制是01111。hash 15只保留hash的低 4 位结果范围正好是 0 到 15。重要前提size 必须是 2 的幂。如果 size 17hash 16的结果完全不能代表余数。这个写法只适合“桶数量固定为 2 的幂”的场景。6. 综合演示一次跑通全部典型用法把前一小节的多个场景放在同一个程序里方便整体验证#include stdio.h int main(void) { unsigned int x 0xD6; // 1101 0110 unsigned int y 0x5A; // 0101 1010 printf(x y 0x%02X\n, x y); printf(x 的最低一位 %d\n, x 1); printf(x 的第 3 位 %d\n, (x 3) 1); printf(x 的低 4 位 0x%X\n, x 0x0F); printf(x 的高 4 位 0x%X\n, (x 4) 0x0F); unsigned int reg 0xFFFF; reg ~0x08; printf(清除第 3 位后的 reg 0x%04X\n, reg); unsigned int test 16; printf(16 是 2 的幂\n%s\n, (test ! 0 (test (test - 1)) 0) ? yes : no); return 0; }期望输出x y 0x52 x 的最低一位 0 x 的第 3 位 0 x 的低 4 位 0x6 x 的高 4 位 0xD 清除第 3 位后的 reg 0xFFF7 16 是 2 的幂 yes建议先自己手算一遍再运行程序对比。如果结果不一致大概率是某一步二进制换算出了问题。只要把十六进制展开成二进制逐位按“全 1 才 1”的规则计算就能找到错误。7. 优先级陷阱最容易扣分的几个点7.1的优先级高于这是位运算里最经典的坑。C 语言中相等运算符的优先级高于按位与。所以if (x 1 0) // 实际解析为 x (1 0)即 x 01 0的结果是 0x 0恒等于 0。条件永远为假代码会一直走 else 分支。正确写法是if ((x 1) 0)如果使用 GCC 并开启-Wall编译器会给出警告suggest parentheses around comparison in operand of 这个警告非常重要不要忽略。它往往说明你写了一个和预期完全不同的表达式。7.2 经典权限判断错误继续 5.5 的例子。有人想检查“没有写权限”时
返回列表