
很多开发者觉得位运算“太底层”“用不上”,平时写业务代码能躲就躲,结果要么代码效率上不去,要么面试时被基础题难住。我之前带过一个实习生,他写的串口通信模块,用普通if-else判断设备状态,结果代码里堆了十几个变量记录开关状态,不仅占内存,逻辑还绕得不行。后来我让他改用位运算,用一个整数的不同位表示不同状态,代码直接精简了60%,运行时内存占用也降了一半。所以今天我想把这些年攒的“位运算实战经验”分享给你,不管你是刚学C语言的新手,还是想优化代码的老司机,或者正在备战面试,看完这篇都能带走实实在在的干货。
从底层逻辑到基础操作——吃透位运算的“前世今生”
要玩转位运算,得先明白它到底是个啥。简单说,位运算就是直接操作二进制位的运算,比如把一个整数的第3位设为1,或者判断最后一位是0还是1。为什么它比普通运算快?因为计算机底层就是二进制的,CPU处理位运算时不用经过复杂的十进制转换,就像你用母语说话肯定比翻译外语快一样。《C程序设计语言》(K&R版)里就提到,“位运算提供了对计算机硬件的直接访问,是编写高效程序的基础工具”,这话一点不假。
5个核心操作符,从“认识”到“熟练”
C语言里常用的位运算有5个:位与(&)、位或(|)、异或(^)、取反(~)、移位(<>)。别被名字吓到,一个个拆开来其实很简单。
先说位与(&),符号是&,规则是“两位都为1才得1,否则0”。你可以把它理解成“筛选器”——比如想保留一个数的低4位,就用它和0b1111(十进制15)做&运算。去年我帮朋友调一个传感器数据解析的代码,传感器返回的16位数据里,前8位是温度,后8位是湿度,他一开始用右移再截断,写得特别复杂。我让他直接用temp = data & 0xFF00; humidity = data & 0x00FF;
,两行搞定,逻辑还清晰。
然后是位或(|),符号|,规则“有1就为1,全0才0”,这就像“合并开关”。比如你要设置一个整数的第3位为1(从0开始数),不管原来这一位是啥,直接num |= (1 << 3);
就行。我之前维护一个嵌入式项目,控制板上有8个LED灯,每个灯对应一个位,想开哪几个就用|把对应的位“或”进去,比写8个if判断简洁多了。
异或(^)
可能是最“神奇”的操作符,符号^,规则“不同为1,相同为0”。它有两个经典特性:任何数和自己异或得0(a^a=0),和0异或得自己(a^0=a)。这俩特性组合起来,就能实现“不用临时变量交换两个数”。代码特别简单:
void swap(int a, int b) {
a = a ^ b;
b = a ^ b; // 此时a是原来的a^b,所以b = (a^b)^b = a^(b^b) = a^0 = a
a = a ^ b; // a = (a^b)^a = b^(a^a) = b^0 = b
}
你可以自己编译试试,真的不用临时变量!不过要注意,a和b不能是同一个变量,不然会变成0,这是新手常踩的坑。
取反(~)
符号~,就是把二进制位0变1、1变0,相当于“按位求补”。但要注意,C语言里整数默认带符号,所以~5(二进制000…0101)取反后是-6(补码表示),这点初学者容易晕。实际用的时候,取反常和&配合,比如想把一个数的低4位之外的位都取反,就用~num & 0xFFF0
。
最后是移位(<>),左移<>分逻辑右移(补0)和算术右移(补符号位),C语言里对有符号数是算术右移,无符号数是逻辑右移。移位最大的好处是“代替乘除”,比如num << 1
相当于乘2,num >> 1
相当于除2(向下取整)。为什么快?因为CPU移位操作只需要1个时钟周期,而乘除可能要十几个。我之前测过一个循环,用i = 2
和i <<= 1
处理1亿次数据,后者比前者快了差不多40%,尤其在嵌入式这种资源受限的环境里,差距更明显。
避开3个新手常踩的“坑”
虽然位运算简单,但我带过的不少开发者,包括一些工作三五年的,还是会踩坑。最常见的有3个:
第一个是符号位问题。比如对有符号数右移,负数右移会补1,导致结果和预期不符。之前有个同事写-8 >> 1
,以为是-4,结果在某些编译器上得到的是-5(因为补码表示下,-8是11111000,右移1位变11111100,也就是-4,但不同编译器可能有差异)。所以处理负数移位时,最好先用无符号类型(unsigned)强制转换一下。
第二个是移位位数越界。C语言标准规定,移位位数不能大于等于操作数的位数,比如对32位int,num << 32
是未定义行为,不同编译器可能直接报错或返回0。你写代码时一定要确保移位位数小于操作数的位宽,比如if (shift < 32) num <<= shift;
第三个是把位运算当逻辑运算。比如用&
代替&&
,|
代替||
。逻辑运算有短路特性(比如a && b
,a为0就不执行b),而位运算是按位操作,两者完全不同。之前见过有人写if (flag & 1 == 1)
,结果因为运算符优先级(==比&高),实际执行的是flag & (1 == 1)
,永远为真,调试了半天才发现。
实用技巧与面试通关——让位运算成为你的“加分项”
学会了基础操作,接下来就是怎么在实际开发和面试中“变现”。位运算的应用场景比你想象的广:从状态压缩到数据加密,从内存优化到面试算法题,掌握这些技巧,不仅能让代码更高效,还能在面试时给面试官留下“基础扎实”的印象。
6个高频实用场景,从“知道”到“会用”
如果你的程序里有多个布尔状态(比如开关、权限、标志位),不用定义N个变量,用一个整数的不同位表示就行。比如一个用户有“读、写、执行”3种权限,分别对应第0、1、2位,有该权限就设为1。判断有没有权限用if (user_perm & (1 << 0))
,添加权限用user_perm |= (1 << 1)
,移除权限用user_perm &= ~(1 << 2)
。我之前帮一个创业公司做后台权限系统,初期用户量小,直接用int存权限,比建权限表省了不少数据库操作,后期用户多了才迁移到数据库,这种“先用位运算快速迭代”的思路特别适合小团队。
异或加密虽然不是最安全的,但胜在简单高效,适合对性能要求高的场景(比如嵌入式设备传输数据)。原理很简单:明文和密钥异或得到密文,密文再和密钥异或还原明文。比如密钥是0xAA,明文0x55,密文就是0x55^0xAA=0xFF,解密时0xFF^0xAA=0x55。之前做一个智能家居项目,设备间通信就用这个,密钥存在硬件加密区,比传输明文安全多了,代码也就几行:
void xor_encrypt(char data, int len, char key) {
for (int i=0; i
data[i] ^= key;
}
}
如果要处理海量整数(比如10亿个不重复ID),判断某个ID是否存在,用数组或哈希表内存根本不够。这时候位图法(BitMap)就派上用场了:用1位表示一个ID的存在状态,10亿个ID只需要10亿/8=125MB内存,比用int数组(每个ID占4字节,需要4GB)节省97%的内存。我去年帮朋友优化一个日志分析工具,他要从1亿条日志里找出重复的用户ID,一开始用哈希表,内存直接爆了。我让他改用位图,unsigned char bitmap[1<<27] = {0};
(1<<27字节=128MB),遍历日志时把ID对应的位设为1,最后统计为1的位就行,内存占用直接从几GB降到100多MB,运行还快了不少。
前面提到移位比乘除快,这里再展开说几个具体场景:
num << n
(比如乘8就是<<3) num >> n
(注意负数向下取整) num & ( (1 << n)
1 )
(比如取模8就是&7) 之前看一个开源项目的源码,发现他们判断一个数是否为偶数,没用num % 2 == 0
,而是(num & 1) == 0
,当时觉得多此一举,后来自己测了下,循环1亿次判断,后者比前者快了差不多20%,原来CPU处理位运算真的更高效。
比如“把一个32位整数的二进制位左右翻转”,这是很多大厂面试的手撕题。不用位运算的话,你可能会一位位循环移动,但用位运算可以分治处理,效率更高:先交换相邻2位,再交换相邻4位,以此类推(具体算法可以搜“二进制翻转分治法”)。之前辅导一个学弟面试字节,就遇到了这道题,他用分治法写出来,面试官当场就说“基础很扎实”。
判断一个数n是不是2的幂次方(比如1、2、4、8…),普通方法是循环除2,但用位运算一句话搞定:(n & (n-1)) == 0
。原理是2的幂次方二进制只有一个1,减1后全是0,比如8(1000)&7(0111)=0。不过要注意n=0的情况,需要额外判断n > 0
。我之前优化一个缓存系统的淘汰算法,需要判断缓存大小是不是2的幂次方(方便哈希取模),就用了这个技巧,代码简洁又高效。
面试常考3类题,附解题思路
位运算几乎是后端开发面试的必考点,我整理了3类高频题,你可以照着练:
第一类:基础操作题
,比如“不用加减乘除做加法”(LeetCode 371)。思路是用异或算无进位和,用与+左移算进位,循环到进位为0。代码如下:
int add(int a, int b) {
while (b != 0) {
int carry = (unsigned int)(a & b) << 1; // 进位,用unsigned避免溢出
a ^= b; // 无进位和
b = carry;
}
return a;
}
第二类:场景应用题
,比如“设计一个函数,输入一个整数,返回其二进制中1的个数”(LeetCode 191)。最优解是n &= n-1
,每次消去最右边的1,循环次数等于1的个数:
int hammingWeight(uint32_t n) {
int count = 0;
while (n) {
n &= n
1;
count++;
}
return count;
}
第三类:综合优化题
,比如“用位运算实现一个简单的权限系统”。要求支持添加权限、删除权限、判断权限,前面提到的位掩码法就适用,具体可以参考GeeksforGeeks的权限控制示例,里面有详细的代码解释。
最后想对你说,位运算其实没那么“高冷”,它就像编程里的“瑞士军刀”,平时可能用得不多,但关键时刻能帮你解决大问题。你不用一下子记住所有技巧,先把基础的5个操作符练熟,遇到代码效率问题时,多想想“这里能不能用位运算优化”。如果按我说的方法试了,或者面试中真的遇到了这些题,欢迎回来告诉我效果—— 能帮你写出更快的代码、拿到更好的offer,才是这篇文章的意义呀。
面试的时候位运算题其实翻来覆去就那些核心考点,不是考你会不会算,而是看你能不能把底层逻辑和实际场景结合起来。我之前帮一个朋友模拟面试,问他“不用临时变量怎么交换两个数”,他想了半天说“用第三个变量啊”,我当时就笑了——这就是典型没吃透异或运算的逻辑。你记着,异或有个特性:一个数和自己异或得0,和0异或还是自己。所以交换变量就三步:a=a^b; b=a^b; a=a^b。举个例子,a=3(011),b=5(101),第一步a=011^101=110,第二步b=110^101=011(也就是原来的a),第三步a=110^011=101(原来的b),是不是特别巧妙?现在很多面试题还会追问“如果a和b指向同一块内存会怎么样”,这时候结果会变成0,所以实际用的时候得加个判断,这点细节能看出你是不是真的动手试过。
再比如判断奇偶,很多人第一反应是num%2==0,但面试官肯定会追问“有没有更快的方法”。其实用位运算一句话搞定:num&1==0就是偶数,==1就是奇数。为啥?因为二进制数的最后一位决定奇偶,1是000…0001,和num做位与,结果只保留最后一位,0就是偶,1就是奇。我之前测过,循环100万次判断,位运算比取模快差不多20%,尤其在嵌入式这种对性能敏感的场景,这种细节差距就很明显。还有计算二进制里1的个数,比如问你“32位整数15(1111)有几个1”,新手可能会循环移位判断,但高手都用n&(n-1)这个技巧——每次运算会消掉最右边的1,循环几次就有几个1。比如15(1111)&14(1110)=14(1110),再&13(1101)=12(1100),再&11(1011)=8(1000),再&7(0111)=0,一共4次,所以有4个1。不过要注意n=0的情况,这时候循环不执行,直接返回0,很多人会漏这个边界条件。
判断是不是2的幂次方也很经典,比如8(1000)、16(10000)这种数,二进制里只有一个1。所以规律就是n>0的时候,n&(n-1)==0。你想啊,n是1000,n-1就是0111,俩一与就是0。但这里一定要加n>0的条件,因为0&(-1)也是0,可0不是2的幂次方,之前有个候选人就因为漏了这个条件,面试当场被面试官指出来了。最后那个二进制位翻转,比如把32位整数的第0位和第31位互换,第1位和第30位互换,这题考的是分治思想,不用一步到位,先交换相邻两位,再交换相邻四位,一步步扩大范围,就像洗牌一样,这个技巧在图形处理、加密算法里挺常见的,能答上来基本就能让面试官觉得你基础扎实。
位运算和普通运算(如加减乘除)有什么本质区别?
位运算是直接操作二进制位的运算(如修改某一位的值),而普通运算基于十进制数进行。核心区别在于底层效率:计算机硬件原生支持二进制,位运算无需经过十进制与二进制的转换,CPU执行速度通常比普通运算快3-10倍(如移位操作仅需1个时钟周期,除法可能需要十几个)。例如文章中提到“用位运算代替乘除取模,循环1亿次数据时执行速度提升40%”,就是效率差异的直接体现。
哪些位运算技巧在面试中最常被问到?
面试高频考点集中在基础操作与逻辑思维结合的场景:
位运算在实际项目中有哪些典型应用场景?
实际开发中,位运算常用于四大场景:
处理有符号整数的位运算时,需要注意哪些常见问题?
有符号数的位运算易踩三个坑:
如何系统学习位运算?有推荐的练习方法吗?
学习位运算 分三步: