最近在帮很多刚学 C 语言的同学梳理位运算相关的知识点,发现很多新手对这部分内容一知半解,尤其是负数的位运算、逗号表达式这些,很容易踩坑。
今天我就把几个最常见的位运算相关的经典案例,从原理到代码,给大家讲透,帮你彻底搞懂这些知识点,看完这篇,再也不用对位运算犯迷糊了!
一、搞懂异或运算:为什么 3 ^ (-5) = -8?
很多新手刚接触异或的时候,都会被这个问题搞懵:
c
#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 = 0 -
0 ^ 1 = 1 -
1 ^ 0 = 1 -
1 ^ 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
-
第二步:按位异或
dns
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 加减法实现
c
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 异或运算实现(推荐)
这就是我们上一节讲的异或的经典用法,没有溢出问题:
c
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 的个数:
c
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 算法,效率最高
希望这篇文章能帮你彻底搞懂这些位运算的知识点,如果你还有其他疑问,欢迎在评论区留言讨论!