15 KiB
3.2 二进制运算
一、为什么还要学“二进制运算”?
在上一节里,我们已经知道了计算机用二进制存数,也知道了补码能把减法变成加法。接下来要学的是:
- 按位与(AND)
- 按位或(OR)
- 按位非(NOT)
- 按位异或(XOR)
- 左移(
<<) - 右移(
>>) - 二进制加法
- 二进制减法
这些运算不是“冷知识”,而是程序里常见的底层工具:权限开关、状态压缩、快速比较、数据校验都会用到。
二、先记住 6 个位运算
设两个二进制位分别是 a 和 $b$,每一位只可能是 0 或 1。
1. 按位与 AND(符号 &)
规则:只有两位都为 1,结果才是 1。
| a | b | a & b |
|---|---|---|
| 0 | 0 | 0 |
| 0 | 1 | 0 |
| 1 | 0 | 0 |
| 1 | 1 | 1 |
可以理解为“都同意才通过”。
2. 按位或 OR(符号 |)
规则:只要有一位是 1,结果就是 1。
| a | b | a | b |
|---|---|---|
| 0 | 0 | 0 |
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 1 | 1 | 1 |
可以理解为“有人同意就通过”。
3. 按位非 NOT(符号 ~,一元运算)
规则:0 变 1,1 变 0。
| a | ~a |
|---|---|
| 0 | 1 |
| 1 | 0 |
4. 按位异或 XOR(符号 ^)
规则:两位不同为 1,相同为 0。
| a | b | a ^ b |
|---|---|---|
| 0 | 0 | 0 |
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 1 | 1 | 0 |
可以理解为“不同就亮灯”。
5. 左移(符号 <<)
规则:x << k 表示所有位向左移动 k 位,右侧补 0。
- 在不溢出的情况下,数值相当于乘 $2^k$。
- 固定 32 位时,左边移出去的高位会被丢弃。
例:8 位下 00101101 << 2 = 10110100。
6. 右移(符号 >>)
规则:x >> k 表示所有位向右移动 k 位。
- 对无符号数,左侧补 0。
- 对有符号数,很多语言会做“算术右移”(补符号位),所以课堂里先按无符号来理解更稳妥。
例:8 位下 00101101 >> 3 = 00000101。
三、C++ 里位运算的优先级(一定要加括号)
下面按 cppreference 的顺序,只保留“和计算最相关”的部分(第 5-7、9-15 级):
| 优先级(高 -> 低) | 运算符 | 含义 | |
|---|---|---|---|
| 5 | * / % |
乘、除、模 | |
| 6 | + - |
加、减 | |
| 7 | << >> |
左移、右移 | |
| 9 | < <= > >= |
关系比较 | |
| 10 | == != |
相等比较 | |
| 11 | & |
按位与 | |
| 12 | ^ |
按位异或 | |
| 13 | | |
按位或 | |
| 14 | && |
逻辑与 | |
| 15 | || |
逻辑或 |
看 5 个典型例子:
a + b << 1
- 实际等价于
(a + b) << 1 - 不是
a + (b << 1)
x | y & z
- 实际等价于
x | (y & z) - 不是
(x | y) & z
a ^ b & c
- 实际等价于
a ^ (b & c) - 不是
(a ^ b) & c
x + y > z
- 实际等价于
(x + y) > z - 不是
x + (y > z)
a & b == 0
- 实际等价于
a & (b == 0)(因为==高于&) - 想判断“按位与结果是否为 0”,应写成
(a & b) == 0
课堂建议:只要一个表达式里混用了两类以上运算符,就主动加括号,别赌记忆。
四、位串上的运算:逐位独立进行
例如:
a = 00101101
b = 00010111
逐位计算:
a & b = 00000101
a | b = 00111111
a ^ b = 00111010
~a = 11010010
注意:~a 的结果长度和机器位数有关。课堂里我们常固定成 8 位或 32 位来讨论。
竖式写法示范(像小学列竖式)
把高位写在左边、低位写在右边,同一列对齐后逐列运算。
- 按位与
&
0 0 1 0 1 1 0 1 (a)
& 0 0 0 1 0 1 1 1 (b)
----------------------
0 0 0 0 0 1 0 1 (a & b)
- 按位或
|
0 0 1 0 1 1 0 1 (a)
| 0 0 0 1 0 1 1 1 (b)
----------------------
0 0 1 1 1 1 1 1 (a | b)
- 按位异或
^
0 0 1 0 1 1 0 1 (a)
^ 0 0 0 1 0 1 1 1 (b)
----------------------
0 0 1 1 1 0 1 0 (a ^ b)
- 左移与右移也可按“横向挪位”理解
a : 0 0 1 0 1 1 0 1
a << 2 : 1 0 1 1 0 1 0 0 (左移两格,右侧补 0)
a >> 3 : 0 0 0 0 0 1 0 1 (右移三格,左侧补 0)
五、二进制加法与减法
1. 二进制加法
和十进制竖式一样,也是“本位求和 + 进位”。
单个位相加规则:
- $0+0=0$,进位 0
- $0+1=1$,进位 0
- $1+0=1$,进位 0
- $1+1=0$,进位 1
如果再加上原来的进位,就变成三数相加。
竖式例子(8 位):00101101 + 00010111
进位: 0 0 1 1 1 1 1
0 0 1 0 1 1 0 1
+ 0 0 0 1 0 1 1 1
----------------------
0 1 0 0 0 1 0 0
可让学生从最右列开始,逐列写“本列结果位”和“向左进位”。
2. 二进制减法
可以按“借位法”逐位减,也可以用补码思想把减法变加法。
在本章练习里,统一按 32 位无符号整数 处理:
- 超过
2^{32}-1的高位进位丢弃 - 不足 0 时按 32 位环绕(相当于加上 $2^{32}$)
竖式例子(8 位):00101101 - 00010111
借位: 0 0 1 1 1 1 0
0 0 1 0 1 1 0 1
- 0 0 0 1 0 1 1 1
----------------------
0 0 0 1 0 1 1 0
从最右列开始,若不够减就向左借 1(借 1 相当于当前位加 2)。
六、C++:用数组模拟按位运算与加减
下面给一份课堂版代码:
#include <bits/stdc++.h>
using namespace std;
const int LEN = 32;
// 把无符号整数转成二进制数组:bits[0] 是最低位
void toBits(unsigned int x, int bits[]) {
for (int i = 0; i < LEN; i++) {
bits[i] = x % 2;
x /= 2;
}
}
// 把二进制数组转回无符号整数
unsigned int toUInt(const int bits[]) {
unsigned int x = 0;
for (int i = LEN - 1; i >= 0; i--) {
x = x * 2 + bits[i];
}
return x;
}
// 打印 32 位二进制串(高位在前)
void printBits(const int bits[]) {
for (int i = LEN - 1; i >= 0; i--) {
cout << bits[i];
}
cout << '\n';
}
void bitAnd(const int a[], const int b[], int c[]) {
for (int i = 0; i < LEN; i++) c[i] = a[i] & b[i];
}
void bitOr(const int a[], const int b[], int c[]) {
for (int i = 0; i < LEN; i++) c[i] = a[i] | b[i];
}
void bitXor(const int a[], const int b[], int c[]) {
for (int i = 0; i < LEN; i++) c[i] = a[i] ^ b[i];
}
void bitNot(const int a[], int c[]) {
for (int i = 0; i < LEN; i++) c[i] = 1 - a[i];
}
// 32 位无符号加法:溢出进位自动丢弃
void addBits(const int a[], const int b[], int c[]) {
int carry = 0;
for (int i = 0; i < LEN; i++) {
int sum = a[i] + b[i] + carry;
c[i] = sum % 2;
carry = sum / 2;
}
}
// 32 位无符号减法:逐位借位
void subBits(const int a[], const int b[], int c[]) {
int borrow = 0;
for (int i = 0; i < LEN; i++) {
int cur = a[i] - b[i] - borrow;
if (cur >= 0) {
c[i] = cur;
borrow = 0;
} else {
c[i] = cur + 2;
borrow = 1;
}
}
}
int main() {
unsigned int A, B;
cin >> A >> B;
int a[LEN], b[LEN], c[LEN];
toBits(A, a);
toBits(B, b);
cout << "A = "; printBits(a);
cout << "B = "; printBits(b);
bitAnd(a, b, c);
cout << "A AND B= "; printBits(c);
bitOr(a, b, c);
cout << "A OR B= "; printBits(c);
bitXor(a, b, c);
cout << "A XOR B= "; printBits(c);
bitNot(a, c);
cout << "NOT A = "; printBits(c);
addBits(a, b, c);
cout << "A + B = " << toUInt(c) << '\n';
subBits(a, b, c);
cout << "A - B = " << toUInt(c) << '\n';
return 0;
}
七、练习题
【第一组】按位运算热身(进阶版,8 位)
已知:
a = 00101101b = 00010111
- 求
a & b。 - 求
a | b。 - 求
a ^ b。 - 求
~a。 - 求
~b。 - 求
(a & b) ^ a。 - 求
(a | b) ^ b。 - 求
(a ^ b) & a。 - 求
(a ^ b) | b。 - 求
(~a) & b。 - 求
(~b) | a。 - 求
a & (~b)。 - 求
(a | b) & (a ^ b)。
再设:
c = 01011000d = 00110101
- 求
c & d。 - 求
c | d。 - 求
c ^ d。 - 求
(c ^ d) ^ c。 - 求
(c & d) | (c ^ d)。 - 求
~(c ^ d)。 - 求
(~c) ^ d。
再做移位:
- 求
a << 1(8 位)。 - 求
a << 3(8 位)。 - 求
a >> 2(8 位)。 - 求
b >> 1(8 位)。 - 求
(a << 2) & 0b11111111(8 位掩码保留)。 - 求
(b << 1) ^ (a >> 2)(8 位)。
【第二组】真值与规律(先判断,再写一句理由)
- 对任意位串
x,是否总有x ^ x == 0? - 对任意位串
x,是否总有x & x == x? - 对任意位串
x,是否总有x | x == x? - 对任意位串
x,是否总有x ^ 0 == x? - 对任意位串
x,是否总有x ^ FULL_MASK == ~x?(其中FULL_MASK指整个位宽全 1) - 对任意位串
x,是否总有(~x) & x == 0? - 对任意位串
x,是否总有(~x) | x == FULL_MASK? - 对任意非负整数
x,是否总有(x << 1) == 2 * x? - 对任意非负整数
x,是否总有(x >> 1) == x / 2(整除)? - 若
x是 2 的幂,是否总有x > 0 && (x & (x - 1)) == 0?
【第三组】加减法进阶(8 位,丢弃溢出位)
- 计算:
00000101 + 00000110 - 计算:
11111111 + 00000001 - 计算:
00001010 - 00000011 - 计算:
00000000 - 00000001 - 计算:
10000000 + 10000000 - 计算:
01111111 + 00000001 - 计算:
01010101 + 00110011 - 计算:
00100000 - 00011111 - 计算:
00010000 - 00100000 - 计算:
10101010 - 01010101
【第四组】逆向构造题(更像算法题)
设 a = 11001010,求一个 8 位 x,使得:
a & x == 10001000(若有多解,写出任意一个)a | x == 11101110(若有多解,写出任意一个)a ^ x == 01100111(写唯一解)
再设 u = 00110110,v = 00000110:
- 构造一个 8 位
y,使得y & u == v。 - 判断是否存在 8 位
z,使得z | u == v,若存在给出一个,若不存在说明原因。
【第五组】常用位技巧(探究题,重点)
这一组不要先背公式,先按题目把例子算出来,再归纳。
A. 探究“最低位 1”(lowbit)
对下面每个 x,先写出二进制,再计算 -x(按补码),最后算出 x & (-x):
-
x = 12 -
x = 40 -
x = 44 -
x = 72 -
观察 52-55 的结果,归纳一句话:
x & (-x)保留了x的哪一部分?
B. 探究“去掉最低位 1”
对下面每个 x,计算 x - 1,再算 x & (x - 1):
-
x = 12 -
x = 40 -
x = 44 -
x = 72 -
观察 57-60 的结果,归纳一句话:
x & (x-1)对二进制位做了什么变化?
C. 由例子得到“2 的幂”判定
分别计算 x & (x - 1),并记录是否为 0:
-
x = 1 -
x = 2 -
x = 4 -
x = 8 -
x = 3 -
x = 6 -
根据 62-67,总结:当
x > 0时,什么条件等价于“x是 2 的幂”?
D. 应用题(把结论用起来)
- 不用循环,判断
64是否是 2 的幂,并写出关键表达式值。 - 不用循环,判断
72是否是 2 的幂,并写出关键表达式值。 - 设
x = 01011000(8 位),先求x & (-x),再给出最低位 1 的位置(最低位记第 0 位)。 - 设
x = 00101000(8 位),先求x & (-x),再给出最低位 1 的位置(最低位记第 0 位)。 - 设
x = 10000000(8 位),先求x & (-x),再给出最低位 1 的位置(最低位记第 0 位)。 - 从
x = 90开始,反复执行x = x & (x - 1)直到变成 0,需要几步?由此得到90的二进制中有几个 1? - 给定偶数
n,写出一个表达式快速判断它是否能被 4 整除,并说明对应的“二进制末尾特征”。
参考答案
第一组
0000010100111111001110101101001011101000001010000010100000101000001111110001001011101101001010000011101000010000011111010110110100110101011111011001001010010010010110100110100000001011000010111011010000100101
第二组
- 是。
- 是。
- 是。
- 是。
- 是(全 1 掩码下逐位翻转)。
- 是。
- 是。
- 在不溢出的前提下是。
- 是(无符号或非负整数语境下)。
- 是。
第三组
00001011(十进制 11)00000000(十进制 0)00000111(十进制 7)11111111(十进制 255)00000000(十进制 0)10000000(十进制 128)10001000(十进制 136)00000001(十进制 1)11110000(十进制 240)01010101(十进制 85)
第四组(给一种可行解)
- 可取
x = 10001100。 - 可取
x = 01100100。 - 唯一解
x = 10101101。 - 可取
y = 11100110(只要在u为 1 的位上与v对齐即可,u为 0 的位任意)。 - 不存在,因为按位或不会把
u中的 1 变成 0,而u = 00110110在第 5、4、2、1 位已有 1,但v = 00000110在第 5、4 位是 0。
第五组
12 = 1100,-12(8 位)是11110100,12 & (-12) = 0100(十进制 4)40 = 101000,40 & (-40) = 001000(十进制 8)44 = 101100,44 & (-44) = 000100(十进制 4)72 = 1001000,72 & (-72) = 0001000(十进制 8)- 结论:
x & (-x)只保留最低位的 1,其余位清零(即 lowbit) 12 & 11 = 1100 & 1011 = 1000(十进制 8)40 & 39 = 101000 & 100111 = 100000(十进制 32)44 & 43 = 101100 & 101011 = 101000(十进制 40)72 & 71 = 1001000 & 1000111 = 1000000(十进制 64)- 结论:
x & (x-1)会把最低位 1 消掉,且更低位保持为 0 1 & 0 = 02 & 1 = 04 & 3 = 08 & 7 = 03 & 2 = 2(非 0)6 & 5 = 4(非 0)- 结论:
x > 0 && (x & (x - 1)) == 0当且仅当x是 2 的幂 64 & 63 = 0,所以是 2 的幂72 & 71 = 64,非 0,所以不是 2 的幂- lowbit 是
00001000(8),最低位 1 在第 3 位 - lowbit 是
00001000(8),最低位 1 在第 3 位 - lowbit 是
10000000(128),最低位 1 在第 7 位 90 -> 88 -> 80 -> 64 -> 0,共 4 步,所以有 4 个 1- 可用
(n & 3) == 0,对应二进制末两位为00