最近在帮很多刚学 C 语言的同学梳理位运算相关的知识点,发现很多新手对这部分内容一知半解,尤其是负数的位运算、逗号表达式这些,很容易踩坑。
今天我就把几个最常见的位运算相关的经典案例,从原理到代码,给大家讲透,帮你彻底搞懂这些知识点,看完这篇,再也不用对位运算犯迷糊了!
一、搞懂异或运算:为什么 3 ^ (-5) = -8?
很多新手刚接触异或的时候,都会被这个问题搞懵:
#include <stdio.h> int main() { int a = 3; int b = -5; int c = a ^ b; printf("%d\n", c); // 输出:-8 return 0; }正数和负数做异或,结果为什么是 - 8?这背后的核心,就是补码。
1.1 异或的基本规则
异或运算符^的规则很简单:相同为 0,相异为 1
0 ^ 0 = 00 ^ 1 = 11 ^ 0 = 11 ^ 1 = 0
但是要注意:C 语言中所有的位运算,都是基于补码来计算的,不是原码!
1.2 负数的补码:位运算的核心
正数的原码、反码、补码都是一样的,但是负数不一样:
原码:最高位是符号位,1 表示负数,其余位是数值
反码:符号位不变,其余位取反
补码:反码 + 1
所有的位运算,都是用补码来计算的,这是新手最容易忽略的点!
1.3 3 ^ (-5) 的完整计算过程
我们以 32 位 int 为例,一步步计算:
第一步:把两个数转成补码
3 的补码(正数):
00000000 00000000 00000000 00000011-5 的补码:
原码:
10000000 00000000 00000000 00000101反码:
11111111 11111111 11111111 11111010补码:
11111111 11111111 11111111 11111011
第二步:按位异或
00000000 00000000 00000000 00000011 // 3的补码 ^ 11111111 11111111 11111111 11111011 // -5的补码 --------------------------------------- 11111111 11111111 11111111 11111000 // 结果的补码第三步:把结果补码转回十进制
结果的补码是负数,我们要转成原码才能得到十进制:
补码减 1:
11111111 11111111 11111111 11110111符号位不变,其余位取反:
10000000 00000000 00000000 00001000原码对应的十进制就是
-8,和代码输出完全一致!
二、异或的经典用法:不创建临时变量交换两个数
这是面试中最常见的题目:不创建临时变量,交换两个整数的值。
很多新手一开始会写这样的代码:
// 错误写法:用了临时变量,不符合题目要求 int main() { int a = 3; int b = 5; int c = 0; // 这里定义了临时变量,违反了题目要求 printf("交换前:a=%d b=%d\n", a, b); c = a; a = b; b = c; printf("交换后:a=%d b=%d\n", a, b); return 0; }其实有两种不用临时变量的方法,我们一个个来看:
2.1 加减法实现
int main() { int a = 3, b = 5; printf("交换前: a=%d b=%d\n", a, b); a = a + b; b = a - b; a = a - b; printf("交换后: a=%d b=%d\n", a, b); return 0; }✅ 优点:逻辑简单,容易理解 ⚠️ 缺点:如果a和b的值很大,a + b可能会超出int的范围导致溢出。
2.2 异或运算实现(推荐)
这就是我们上一节讲的异或的经典用法,没有溢出问题:
int main() { int a = 3; int b = 5; printf("交换前: a=%d b=%d\n", a, b); a = a ^ b; b = a ^ b; a = a ^ b; printf("交换后: a=%d b=%d\n", a, b); return 0; }✅ 优点:不会溢出,效率高,完全符合题目要求 ⚠️ 注意:如果a和b指向同一个变量(比如传入同一个地址),会把值清为 0,所以只适用于两个独立变量的交换。
2.3 三种方法对比
方法 | 是否需要临时变量 | 是否会溢出 | 适用场景 |
临时变量法 | ✅ 需要 | 不会溢出 | 通用场景,最推荐 |
加减法 | ❌ 不需要 | 可能溢出 | 数值较小的场景 |
异或法 | ❌ 不需要 | 不会溢出 | 两个独立整数交换,面试题常用 |
💡 小提示:实际开发中,临时变量法是最推荐的,代码可读性好,没有溢出风险,也不会有交换同一个变量的坑。不使用临时变量的写法更多是面试题或者趣味用法,不要在生产代码里乱用。
三、逗号表达式:别被 “逗号” 骗了,它的优先级最低!
逗号表达式是很多新手最容易懵的知识点,很多人搞不懂它到底是干嘛的。
逗号表达式的格式:表达式1, 表达式2, 表达式3, ..., 表达式n
执行顺序:从左到右依次执行每个表达式
返回值:整个逗号表达式的结果,是最后一个表达式的值
优先级:逗号运算符是所有运算符中优先级最低的!
我们通过三个经典案例来理解它:
3.1 案例 1:带括号的逗号表达式赋值
执行过程分析
初始状态
变量a和b的初始值分别为1和2。
逗号表达式解析
逗号表达式(a>b, a=b+10, a, b=a+1)按从左到右顺序依次执行:
比较
a > b
计算1 > 2,结果为逻辑假(0),但此结果不参与赋值,仅作为中间步骤被丢弃。
状态保持:a=1,b=2赋值
a = b + 10
计算2 + 10,将结果12赋给a。
更新状态:a=12,b=2取值
a
直接读取a的值12,但结果仍被丢弃。
状态保持:a=12,b=2赋值
b = a + 1
计算12 + 1,将结果13赋给b。
更新状态:a=12,b=13
最终结果
逗号表达式的值为最后一个表达式b=a+1的结果13,因此c被赋值为13。
关键点总结
- 逗号表达式按顺序执行,但仅最后一个子表达式的结果作为整体返回值。
- 中间步骤可能修改变量值(如
a和b的更新),需注意状态变化。 - 表达式
(a>b)和a的计算结果不影响最终赋值。
3.2 案例 2:if 条件中的逗号表达式
if (a = b + 1, c = a / 2, d > 0) { // 业务代码 }
这里的if条件是一个逗号表达式:
先执行
a = b + 1再执行
c = a / 2最后执行
d > 0,整个if条件的真假由d > 0的结果决定
前面两个表达式只是单纯执行,它们的结果不会影响if的判断,只有最后一个表达式决定条件是否成立。
3.3 案例 3:while 循环的逗号表达式优化
原始代码:
a = get_val(); count_val(a); while (a > 0) { // 业务处理 a = get_val(); count_val(a); }
这段代码有重复的逻辑,我们可以用逗号表达式优化成:
while (a = get_val(), count_val(a), a > 0) { // 业务处理 }
效果和原始代码完全等价,而且代码更简洁,避免了重复的函数调用。
3.4 逗号表达式的注意事项
优先级极低:逗号运算符优先级最低,低于赋值运算符,所以很多时候需要加括号,比如案例 1 中如果不加括号,
int c = a>b, a=b+10, a, b=a+1;会被解析成多个独立语句,而不是一个逗号表达式。和函数参数的逗号不一样:函数参数中的逗号(比如
printf(a, b))不是逗号运算符!它只是用来分隔参数的,不会按逗号表达式的规则执行,不要搞混了。适度使用:虽然逗号表达式能让代码更简洁,但过度使用会降低可读性,实际开发中建议适度使用。
四、统计二进制中 1 的个数:从入门到优化,这 3 种写法你都见过吗?
这是算法题中非常经典的题目:输入一个整数,统计它的二进制表示中 1 的个数。
我们从入门到优化,来看三种不同的写法:
4.1 入门写法:取模 + 除法
很多新手一开始会写这样的代码:
int main()
{
int n = 0;
scanf("%d", &n);
int count = 0;
while (n)
{
if (n % 2 == 1)
count++;
n /= 2;
}
printf("%d\n", count);
return 0;
}
核心思路:每次判断最低位是不是 1,然后把 n 右移一位。
但是这段代码有个致命的缺陷:当输入的 n 是负数时,会陷入死循环! 原因:负数右移时高位会补 1,永远不会变成 0,循环永远不会结束。
4.2 进阶写法:移位 + 按位与
为了解决负数的问题,我们可以遍历所有 32 位,逐个判断:
int main() { int n = 0; int count = 0; scanf("%d", &n); int i = 0; for (i = 0; i < 32; i++) // 遍历int的32个bit位 { if (((n >> i) & 1) == 1) // 检查第i位是不是1 count++; } printf("%d\n", count); return 0; }✅ 优点:可以同时处理正数和负数,不会死循环。 ⚠️ 缺点:固定循环 32 次,效率不算最优,比如 n=1,也要循环 32 次。
4.3 最优写法:Brian Kernighan 算法
这是目前最高效的写法,循环次数等于 1 的个数:
int count = 0; unsigned int m = (unsigned int)n; while (m) { m &= (m - 1); // 清除最低位的1 count++; }原理:num & (num - 1)会把二进制中最右边的 1 变成 0,循环执行的次数就等于 1 的个数,效率极高。
比如 n=13(二进制1101):
第一次:
13 & 12 = 12(1101→1100,消除了最后一个 1)第二次:
12 & 11 = 8(1100→1000,消除了中间的 1)第三次:
8 & 7 = 0(1000→0000,消除了第一个 1) 循环结束,count=3,正好是 1 的个数。
4.4 三种方法对比
实现方法 | 支持负数? | 循环次数 | 优点 | 缺点 |
取模 + 除法 | ❌ 不支持 | 最多 32 次 | 逻辑简单 | 负数会死循环 |
移位 + 按位与 | ✅ 支持 | 固定 32 次 | 逻辑清晰,支持负数 | 固定循环次数,效率一般 |
Brian Kernighan 算法 | ✅ 支持 | 等于 1 的个数 | 效率最高,无溢出 | 稍微难理解一点 |
总结
位运算在 C 语言中是非常高效的操作,很多底层开发、算法题中都会用到。新手在学习的时候,一定要注意这几个点:
负数的位运算都是基于补码的,不要用原码去计算
逗号表达式的优先级最低,很多时候需要加括号,而且和函数参数的逗号不一样
无临时变量交换变量只是面试题,实际开发还是用临时变量更安全
统计二进制 1 的个数,优先用 Brian Kernighan 算法,效率最高
希望这篇文章能帮你彻底搞懂这些位运算的知识点,如果你还有其他疑问,欢迎在评论区留言讨论!