Skip to content

4 位运算

4.1 什么是位运算

位运算,顾名思义,就是直接对二进制位进行操作的运算。

在计算机中,所有的数据都是以二进制形式存储的,位运算就是直接对这些二进制位进行操作。

位运算的特点是:

  • 速度快:直接操作二进制位,效率非常高
  • 节省空间:可以用一个位表示开关状态(开/关)
  • 底层操作:很多底层操作都需要用到位运算

为什么需要位运算?

举个栗子,假设你有一个开关,它只有两种状态:开和关。

如果用普通变量存储,可能需要一个字节(8位),但实际上只需要1位就够了。

使用位运算,可以在一个字节中存储8个开关的状态,大大节省内存空间。

另外,位运算在权限控制、数据压缩、加密算法等领域都有广泛应用。

4.2 位运算符

位运算符主要有以下几种:

运算符名称说明
&按位与两位同时为1,结果才为1
|按位或两位只要有一个为1,结果就为1
~按位取反0变1,1变0
^按位异或两位不同,结果为1;相同,结果为0
<<左移各二进制位全部左移若干位
>>右移各二进制位全部右移若干位

下面我们逐一介绍这些运算符。

4.3 按位与(&)

1 运算规则

按位与的运算规则是:两位同时为1,结果才为1,否则为0

第一个数第二个数结果
000
010
100
111

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 01110000 1000 的按位取反结果,这个技巧很常用。

4.4 按位或(|)

1 运算规则

按位或的运算规则是:两位只要有一个为1,结果就为1,否则为0

第一个数第二个数结果
000
011
101
111

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

第一个数第二个数结果
000
011
101
110

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 1011

1111 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 1011
  • 0011 1011 是 59,所以 -20 >>> 2 = 59

这个运算符在处理无符号数(C/C++中有无符号数)时很有用。

4.9 综合练习

让我们通过一个综合实例来巩固一下位运算的知识。

问题:假设有一个字节,用来存储8个开关的状态,第0位表示开关1,第1位表示开关2,以此类推。

请实现以下操作:

  1. 设置第3位为1
  2. 清除第5位
  3. 判断第2位是否为1
  4. 切换第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. 按位或(|):用于设置某一位为1,或者合并标志位
  3. 按位异或(^):用于交换变量、简单加密、切换某一位的状态
  4. 按位取反(~):配合按位与使用,清零某些位
  5. 左移(<<):快速乘法,相当于乘以2的n次方
  6. 右移(>>):快速除法,相当于除以2的n次方

位运算直接操作二进制位,速度快,效率高,掌握位运算对于理解计算机底层原理、优化代码性能都非常有帮助。在实际编程中,位运算经常用于权限控制、数据压缩、加密算法等领域。