整数在计算机里就是一串 0 和 1。位运算直接在这串位上动手,往往比取模、乘除更快更省。判断奇偶只要看最后一位,交换两个数可以不借第三个变量,压缩存储、标志位操作,算法竞赛里到处是它的影子。不少同学一看到 ~、&、|、^ 这些符号就发怵,其实规则没多少,六个运算符加上几个常用组合,大部分场景就够用了。
这一节我们把六个位运算符逐个讲清楚,每个结论都配上二进制对照,方便大家边看边验证,后面再介绍五个最高频的技巧和三个常见误区。读之前只要懂二进制,知道整数在内存里就是一串 0 和 1 就行,别的不用额外准备。
1. 位运算基础
位运算直接作用在整数的二进制位上。我们先认识六个运算符,每个都讲清楚规则和常见用途,这是后面所有技巧的底子。
取反 ~:每个位 0 变 1、1 变 0。它会把正数变成对应的负数补码形式,这一点我们放到第 4 节展开讲。
位与 &:对应两位都为 1,结果才是 1。常用来保留指定位,把其他位清零。
位或 |:对应位只要有一个是 1,结果就是 1。常用来把某一位置成 1。
位异或 ^:两位相同得 0、不同得 1。同一个数异或两次会还原,这个性质正是后面异或交换技巧的基础。
左移 <<:所有位左移,高位丢弃、低位补 0,效果相当于乘 2 的幂。
右移 >>:所有位右移,低位丢弃、高位补符号位,正数补 0、负数补 1,相当于除 2 的幂。
六个运算符的最简写法如下,注释里标了每个运算的二进制含义:
~a // 按位取反 a & b // 按位与 a | b // 按位或 a ^ b // 按位异或 a << n // 左移 n 位(相当于 a * 2^n) a >> n // 右移 n 位(相当于 a / 2^n)
左移的位数 n 是必填参数,右移对有符号数会补符号位,这两个细节我们在演示程序里还会再碰到。
2. 演示程序
规则背下来不算真会,把每个运算符实际跑一遍才踏实。下面这段程序把六个运算符各演示一次,注释里写清了运算前后的二进制变化:
// 1. 取反
void test01()
{
int num1 = 2;
// 000...0010 取反后,负数以补码存储
int ret = ~num1;
printf("ret = %d\n", ret); // 输出 -3
}
// 2. 位与
void test02()
{
int num1 = 2, num2 = 3;
// 0010 & 0011 = 0010
int ret = num1 & num2;
printf("ret = %d\n", ret); // 输出 2
}
// 3. 位或
void test03()
{
int num1 = 2, num2 = 3;
// 0010 | 0011 = 0011
int ret = num1 | num2;
printf("ret = %d\n", ret); // 输出 3
}
// 4. 位异或
void test04()
{
int num1 = 2, num2 = 3;
// 0010 ^ 0011 = 0001
int ret = num1 ^ num2;
printf("ret = %d\n", ret); // 输出 1
}
// 5. 左移
void test05()
{
int num = 2;
// 0010 << 2 = 1000
int ret = num << 2;
printf("ret = %d\n", ret); // 输出 8
}
// 6. 右移
void test06()
{
int num = 11;
// 1011 >> 2 = 0010
int ret = num >> 2;
printf("ret = %d\n", ret); // 输出 2
}
我们逐个看输出。test01 里 2 取反得到 -3,因为负数在内存里以补码存储,~2 的结果按补码来解释就是 -3。test02 到 test04 是三个按位组合运算,2 和 3 分别是 0010 和 0011,与出来是 0010 也就是 2,或出来是 0011 也就是 3,异或出来是 0001 也就是 1。test05 把 2 左移两位,0010 变成 1000 也就是 8,正好等于乘 4。test06 把 11 右移两位,1011 的末两位被移出,剩下 0010 也就是 2,相当于 11 除以 4 向下取整。
3. 常用技巧
运算符本身不难,难在怎么组合出有用的操作。下面五个是最高频的技巧,代码和注释都带二进制说明:
#include <limits.h>
// 1. 判断奇偶:看最后一位是否为 1
void test()
{
int num = 3;
printf("%s\n", (num & 1) == 1 ? "奇数" : "偶数"); // 奇数
}
// 2. 获得整型最大值
void test()
{
printf("%d\n", INT_MAX); // 0111...1111 = 2147483647
printf("%d\n", INT_MAX); // 同上
}
// 3. 快速乘 2 / 除 2
void test()
{
int num = 6;
printf("%d\n", num << 1); // 12,相当于 *2
printf("%d\n", num >> 1); // 3,相当于 /2
}
// 4. 从低位到高位,取 v 的第 n 位
void test()
{
int v = 8, n = 4; // 取 v 的第 4 位
printf("%d\n", (v >> (n - 1)) & 1); // 输出 1
}
// 5. 不用临时变量交换两个数(异或交换)
void test()
{
int num1 = 2, num2 = 3;
num1 = num1 ^ num2;
num2 = num1 ^ num2;
num1 = num1 ^ num2;
printf("num1=%d num2=%d\n", num1, num2); // num1=3 num2=2
}
第一个技巧是判断奇偶。只要看二进制最后一位,num & 1 等于 1 就是奇数。这比取模快,因为只检查一个位就够了。
第二个技巧是取整型最大值。INT_MAX 就是 2147483647,二进制是 0111…1111。有人喜欢写成 (1 << 31) – 1,想表达同样的意思,但 32 位有符号 int 左移 31 位会触发未定义行为,标准不保证结果。所以这里直接用标准库的 INT_MAX 宏,用之前包含 limits.h 就行。这个坑第 4 节还会细说。
第三个技巧是快速乘除。左移一位相当于乘 2,右移一位相当于除 2,6 左移一位得 12,右移一位得 3。
第四个技巧是取某一位。v 的第 n 位(从 1 开始数)等于 (v >> (n – 1)) & 1:先右移把目标位挪到最低位,再按位与 1 把它提取出来。
第五个技巧是异或交换。借异或的自反性,连续异或三次就把两个数换过来了,全程不用第三个变量。要注意 num1 和 num2 必须是两个不同的变量,这条限制第 4 节会讲。
4. 边界与陷阱
技巧虽好用,出 bug 却往往出在边界条件上。下面四个点要记牢。
负数在内存里是按补码存的。对负数取反、右移时,结果都要按补码来解释,别把二进制位直接当十进制读。比如 ~2 得到的是 -3 而不是 253,因为 int 是有符号 32 位数,取反后的那排位模式按补码解读就是 -3。
移位别越界。左移的位数超过类型本身的位数,结果就是未定义的。32 位 int 左移 31 位及以上就得小心,前面取 INT_MAX 的例子正是这种情况。标准对这类移位不保证任何结果,所以一律避开。
有符号数右移会补符号位。有符号数的右移是算术右移,高位补符号位,负数会往负无穷方向取整,并不是简单除 2。比如 -1 不管右移多少位,结果还是 -1。
异或交换别用在同一个变量上。如果 num1 和 num2 指向同一块内存,第一次异或就把值清成 0,后面再也找不回来。交换之前先确认这两个变量确实是不同的。
5. 常见误区
最后我们澄清三个常见的认识误区。
第一个误区是把位运算当炫技滥用。现代编译器会自动把乘 2 优化成移位,普通代码里优先写清晰的乘 2、除 2,可读性比省几个时钟周期更值钱,位运算留给真正需要它的地方。
第二个误区是把 & 和 &&、| 和 || 弄混。& 是按位与,&& 是逻辑与,一个作用在位上,一个作用在布尔值上,写混了逻辑就全变样了。
第三个误区是取位时位置数错。取第 n 位要先右移 n-1 位,再按位与 1,位数是从 1 开始数的,不是从 0 开始。
小结一下。位运算就是直接操作整数的二进制位,取反、与、或、异或、左移、右移,对应着取反、保留、置位、翻转、乘 2、除 2 这六种能力。常用技巧有按位与判奇偶、用 INT_MAX 取最大值、移位快速乘除、右移加与取某一位、异或不借临时变量交换两数。用的时候记住四点:负数按补码解释、移位别越界、有符号右移补符号位、异或交换要求两个不同变量,再把 & 和 && 分清楚。这几点记牢,位运算的代码就能放心看、放心写了。




冀公网安备13050302001966号