Appearance
4 位运算
4.1 什么是位运算
位运算,顾名思义,就是直接对二进制位进行操作的运算。
在计算机中,所有的数据都是以二进制形式存储的,位运算就是直接对这些二进制位进行操作。
位运算的特点是:
- 速度快:直接操作二进制位,效率非常高
- 节省空间:可以用一个位表示开关状态(开/关)
- 底层操作:很多底层操作都需要用到位运算
为什么需要位运算?
举个栗子,假设你有一个开关,它只有两种状态:开和关。
如果用普通变量存储,可能需要一个字节(8位),但实际上只需要1位就够了。
使用位运算,可以在一个字节中存储8个开关的状态,大大节省内存空间。
另外,位运算在权限控制、数据压缩、加密算法等领域都有广泛应用。
4.2 位运算符
位运算符主要有以下几种:
| 运算符 | 名称 | 说明 |
|---|---|---|
| & | 按位与 | 两位同时为1,结果才为1 |
| | | 按位或 | 两位只要有一个为1,结果就为1 |
| ~ | 按位取反 | 0变1,1变0 |
| ^ | 按位异或 | 两位不同,结果为1;相同,结果为0 |
| << | 左移 | 各二进制位全部左移若干位 |
| >> | 右移 | 各二进制位全部右移若干位 |
下面我们逐一介绍这些运算符。
4.3 按位与(&)
1 运算规则
按位与的运算规则是:两位同时为1,结果才为1,否则为0。
| 第一个数 | 第二个数 | 结果 |
|---|---|---|
| 0 | 0 | 0 |
| 0 | 1 | 0 |
| 1 | 0 | 0 |
| 1 | 1 | 1 |
2 实例演示
举个栗子,计算 5 & 3:
bash
5 的二进制: 0000 0101
3 的二进制: 0000 0011
-----------------------
按位与结果: 0000 0001- 只有第0位两个数都是1,所以结果是
0000 0001,即10进制的1。
5 & 3 = 1 你说这有啥意义?
从10进制的角度来看,这确实没什么意义,位运算并不关心得到的十进制结果,关心的是每一位的状态和代表的含义。也就是在某些情况下,我们需要知道一个字节具体某一位是0还是1。
3 实际应用
按位与最常见的应用是:判断某一位是否为1。
举个栗子:假设有一个字节 0000 1010,我们想判断第3位是否为1(从右往左数,从0开始)。
我们可以构造一个只有第3位是1的数:0000 1000(从第0位开始算),然后进行按位与运算:
bash
0000 1010 & 0000 1000 = 0000 1000- 结果不为0,说明第3位是1。
如果判断第2位:
bash
0000 1010 & 0000 0100 = 0000 0000- 结果为0,说明第2位是0。
另一个应用是清零某些位。
举个栗子:想把 0000 1010 的第3位清零(设置为0):
bash
0000 1010 & 1111 0111 = 0000 0010- 注意,
1111 0111是0000 1000的按位取反结果,这个技巧很常用。
4.4 按位或(|)
1 运算规则
按位或的运算规则是:两位只要有一个为1,结果就为1,否则为0。
| 第一个数 | 第二个数 | 结果 |
|---|---|---|
| 0 | 0 | 0 |
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 1 | 1 | 1 |
2 实例演示
举个栗子:计算 5 | 3:
bash
5 的二进制: 0000 0101
3 的二进制: 0000 0011
-----------------------
按位或结果: 0000 0111- 结果是
0000 0111,即十进制的7。
3 实际应用
按位或最常见的应用是:设置某一位为1。
举个栗子:假设有一个字节 0000 0010,我们想把第3位设置为1:
bash
0000 0010 | 0000 1000 = 0000 1010结果第3位被设置为1了。
另一个应用是合并标志位。
举个栗子:假设有三个权限标志:
- 读权限:
0000 0001(1) - 写权限:
0000 0010(2) - 执行权限:
0000 0100(4)
如果一个用户有读和写权限,可以这样表示:
bash
0000 0001 | 0000 0010 = 0000 0011- 结果
0000 0011就表示同时具有读和写权限。
4.5 按位取反(~)
1 运算规则
按位取反的运算规则是:0变1,1变0。
2 实例演示
举个栗子:计算 ~5:
bash
5 的二进制(原码): 0000 0101
按位取反: 1111 1010注意:前面的章节讲过了,在计算机中的数都是补码,所以 0000 0101 是补码, 1111 1010 也是补码,要转换成原码才能知道是多少:
bash
1111 1010 # 补码
1111 1001 # 反码(-1)
1000 0110 # 原码(除符号位取反)所以 ~5 = -6。
3 实际应用
按位取反最常见的应用是配合按位与运算来清零某些位。
举个栗子:想把 0000 1010 的第3位清零:
bash
# 可以这样:
0000 1010 & 1111 0111 = 0000 0010
# 也可以这样
0000 1010 & ~0000 1000 = 0000 1010 & 1111 0111 = 0000 0010先对
0000 1000取反得到1111 0111,然后按位与,第3位就被清零了。这种写法在编程中很常见,可以灵活地控制某一位的开关状态。
4.6 按位异或(^)
1 运算规则
按位异或的运算规则是:两位不同,结果为1;相同,结果为0。
| 第一个数 | 第二个数 | 结果 |
|---|---|---|
| 0 | 0 | 0 |
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 1 | 1 | 0 |
2 实例演示
举个栗子,计算 5 ^ 3:
bash
5 的二进制: 0000 0101
3 的二进制: 0000 0011
-----------------------
按位异或结果: 0000 0110- 结果是
0000 0110,即十进制的6。
3 实际应用
按位异或有几个很有趣的特性:
特性1:一个数异或自己等于0
bash
0000 0101 ^ 0000 0101 = 0000 0000特性2:一个数异或0等于自己
bash
0000 0101 ^ 0000 0000 = 0000 0101特定3:一个数异或一个数得到的结果,再次异或这个数,得到的原来的数
bash
# 异或得到结果
0000 0101 ^ 0000 0011 = 0000 0110
# 将结果再次异或,得到原来的值
0000 0110 ^ 0000 0011 = 0000 0101特性4:异或运算满足交换律和结合律
a ^ b ^ c = c ^ a ^ b = b ^ c ^ a应用1:交换两个变量的值
我们在编程的时候,经常会交换两个变量的值,需要借助第三个变量:
c
int a = 5;
int b = 3;
// 交换a、b的值,需要借助第三个变量
int temp = a; // temp = 5
a = b; // a = 3
b = temp; // b = 5而通过异或运算,可以不使用第三个变量:
c
int a = 5;
int b = 3;
a = a ^ b // a = 5 ^ 3 = 6
b = a ^ b // b = 6 ^ 3 = 5
a = a ^ b // a = 6 ^ 5 = 3- 最终 a = 3,b = 5,交换成功!但是这种方式用的不多,因为可读性差,不太容易理解。
应用2:简单的加密
假设明文是 0000 0101(5),密钥是 0000 0011(3):
加密:0000 0101 ^ 0000 0011 = 0000 0110(6)
解密:0000 0110 ^ 0000 0011 = 0000 0101(5)
利用的就是上面的特性3,这就是最简单的异或加密算法。
4.7 左移(<<)
1 运算规则
左移的运算规则是:各个位全部左移若干位,高位丢弃,低位补0。
2 实例演示
举个栗子:计算 5 << 2:
bash
5 的二进制: 0000 0101
左移2位: 0001 0100- 结果是
0001 0100,即十进制的20。 - 左移 n 位,相当于乘以
2^n(2的n次方)。
3 实际应用
左移运算最常见的应用是快速乘法。
观察一下:
bash
5 << 1 = 10 # 5 * 2 = 10
5 << 2 = 20 # 5 * 4 = 20
5 << 3 = 40 # 5 * 8 = 40- 可以看出:
a << n等价于a * 2^n。
左移运算比乘法运算快很多,所以在需要快速计算时可以使用左移。
注意:左移可能会导致溢出。
举个栗子:在一个字节中,127 << 1:
bash
127 的二进制: 0111 1111
左移1位: 1111 1110 # 本来符号位是0,现在变成1了1111 1110 是 -2 的补码,所以 127 << 1 = -2,这就溢出了。
4.8 右移(>>)
1 运算规则
右移的运算规则是:各个位全部右移若干位,低位丢弃,高位补符号位。
- 对于正数,高位补0
- 对于负数,高位补1
这种方式是算数右移,还有一种是逻辑右移,下面讲。
2 实例演示
举个栗子:计算 20 >> 2:
bash
20 的二进制: 0001 0100
右移2位: 0000 0101- 结果是
0000 0101,即十进制的5。 - 右移 n 位,相当于除以
2 ^ n(2的n次方)。
再举个负数的例子,计算 -20 >> 2:
bash
-20 的原码: 1001 0100
-20 的反码: 1110 1011
-20 的补码: 1110 1100
右移2位: 1111 10111111 1011 是补码,转换成原码:
bash
1111 1011 # 补码
1111 1010 # 反码(-1)
1000 0101 # 原码(除符号位取反)所以 -20 >> 2 = -5,相当于除以 4 。
3 实际应用
右移运算最常见的应用是快速除法。
观察一下:
bash
20 >> 1 = 10 # 20 / 2 = 10
20 >> 2 = 5 # 20 / 4 = 5
20 >> 3 = 2 # 20 / 8 = 2(向下取整)可以看出:
a >> n等价于a / 2^n(向下取整)。右移运算比除法运算快很多,所以在需要快速计算时可以使用右移。
注意:右移是向下取整的。
举个栗子:5 >> 1 = 2,而不是 2.5。
4 逻辑右移
上面在右移的时候,左边高位补符号位,这种方式称为算数右移。还有一种右移,左边高位空出的位用 0 填充,这种方式称为逻辑右移。
运算符:>>>(无符号右移)
无符号右移时,无论正数还是负数,高位都补0。
举个栗子:-20 >>> 2:
bash
-20 的补码: 1110 1100
无符号右移2位: 0011 10110011 1011是 59,所以-20 >>> 2 = 59。
这个运算符在处理无符号数(C/C++中有无符号数)时很有用。
4.9 综合练习
让我们通过一个综合实例来巩固一下位运算的知识。
问题:假设有一个字节,用来存储8个开关的状态,第0位表示开关1,第1位表示开关2,以此类推。
请实现以下操作:
- 设置第3位为1
- 清除第5位
- 判断第2位是否为1
- 切换第4位的状态
解答:
假设初始状态为 0000 0000(所有开关都关闭)
bash
1. 设置第3位为1:
0000 0000 | 0000 1000 = 0000 1000
2. 清除第5位:
0000 1000 & ~0010 0000 = 0000 1000 & 1101 1111 = 0000 1000
3. 判断第2位是否为1:
0000 1000 & 0000 0100 = 0000 0000
结果为0,说明第2位是0
4. 切换第4位的状态:
0000 1000 ^ 0001 0000 = 0001 1000- 最终状态是
0001 1000,表示第3位和第4位的开关是打开的。
4.10 总结
位运算虽然看起来简单,但是应用非常广泛:
- 按位与(&):用于判断某一位是否为1,或者清零某些位
- 按位或(|):用于设置某一位为1,或者合并标志位
- 按位异或(^):用于交换变量、简单加密、切换某一位的状态
- 按位取反(~):配合按位与使用,清零某些位
- 左移(<<):快速乘法,相当于乘以2的n次方
- 右移(>>):快速除法,相当于除以2的n次方
位运算直接操作二进制位,速度快,效率高,掌握位运算对于理解计算机底层原理、优化代码性能都非常有帮助。在实际编程中,位运算经常用于权限控制、数据压缩、加密算法等领域。