计算机中的位运算

基础

  • 位(bit): 是计算机中最小数据单位。每一位的状态只能是 0 或 1。
  • 字节(byte): 八个二进制位构成一个字节。它是存储空间的基本计量单位。一个字节可以存储一个英文字母。一个汉字占据两个字节的存储空间。
  • 字(word): 由若干个字节构成,字的位数叫做字长,不同的机器有不同的字长。一台八位机,它的一个字就等于一个字节,字长为八位。一台十六位机,一个字由两个字节构成,字长为十六位。字是计算机进行数据处理和运算的单位。

一个字节八比特,就是八个二进制位。四个二进制位最大表示为 15,就是一个十六进制数,所以八位可以表示成两个十六进制数。

整数在内存中的存储

在了解位运算之前首先要了解原码、反码与补码。加法和减法其实可以被合并为一种运算,即加法运算。因为减去一个数相当于加上这个数的相反数,例如,5 - 3 等价于 5 + (-3)10 - (-9) 等价于 10 + 9

相反数是指数值相同,符号不同的两个数,例如,10 和 -10 就是一对相反数,-98 和 98 也是一对相反数。

原码

将一个整数转换成二进制形式,就是其原码。

例如 short a = 6; ,a 的原码就是 0000 0000 0000 0110

更改 a 的值为 a = -18; ,此时 a 的原码就是 1000 0000 0001 0010

通俗的理解,原码就是一个整数本来的二进制形式。

反码

谈到反码,正数和负数要区别对待,因为它们的反码不一样。

对于正数,它的反码就是其原码(原码和反码相同)。

负数的反码是将原码中除符号位以外的所有位(数值位)取反,也就是 0 变成 1,1 变成 0。

例如 short a = 6; ,a 的原码和反码都是 0000 0000 0000 0110

更改 a 的值 a = -18; ,此时 a 的反码是 1111 1111 1110 1101

补码

正数和负数的补码也不一样,也要区别对待。

对于正数,它的补码就是其原码(原码、反码、补码都相同)。

负数的补码是其反码加 1。

例如 short a = 6; ,a 的原码、反码、补码都是 0000 0000 0000 0110

更改 a 的值 a = -18; ,此时 a 的补码是 1111 1111 1110 1110

可以认为,补码是在反码的基础上打了一个补丁,进行了一下修正,所以叫 “补码”。

原码、反码、补码的概念只对负数有实际意义,对于正数,它们都一样。

最后我们总结一下 6 和 -18 从原码到补码的转换过程:

补码

在计算机内存中,整数一律采用补码的形式来存储。这意味着,当读取整数时还要采用逆向的转换,也就是将补码转换为原码。

将补码转换为原码也很简单:先减去 1,再将数值位取反即可。

整数如何进行加减法运算

假设 6 和 18 都是 short 类型的,现在我们要计算 6 - 18 的结果,根据运算规则,它等价于 6 + (-18)

如果采用原码计算,那么运算过程为:

1
2
3
4
6 - 18 = 6 + (-18)
= [0000 0000 0000 0110]原 + [1000 0000 0001 0010]原
= [1000 0000 0001 1000]原
= -24

直接用原码表示整数,让符号位也参与运算,对于类似上面的减法来说,结果显然是不正确的。

于是人们开始继续探索,不断试错,后来设计出了反码。下面就演示了反码运算的过程:

1
2
3
4
5
6 - 18 = 6 + (-18)
= [0000 0000 0000 0110]反 + [1111 1111 1110 1101]反
= [1111 1111 1111 0011]反
= [1000 0000 0000 1100]原
= -12

这样一来,计算结果就正确了。

然而,这样还不算万事大吉,我们不妨将减数和被减数交换一下位置,也就是计算 18 - 6 的结果:

1
2
3
4
5
6
18 - 6 = 18 + (-6)
= [0000 0000 0001 0010]反 + [1111 1111 1111 1001]反
= [1 0000 0000 0000 1011]反
= [0000 0000 0000 1011]反
= [0000 0000 0000 1011]原
= 11

按照反码计算的结果是 11,而真实的结果应该是 12 才对,它们相差了 1 。

多出来的 1 是加法运算过程中的进位,它溢出了,内存无法容纳,所以直接删除。

6 - 18 的结果正确,18 - 6 的结果就不正确,相差 1。按照反码来计算,是不是小数减去大数正确,大数减去小数就不对了,始终相差 1 呢?我们不妨再看两个例子,分别是 5 - 1313 - 5

5 - 13 的运算过程为:

1
2
3
4
5
6
5 - 13 = 5 + (-13)
= [0000 0000 0000 0101]原 + [1000 0000 0000 1101]原
= [0000 0000 0000 0101]反 + [1111 1111 1111 0010]反
= [1111 1111 1111 0111]反
= [1000 0000 0000 1000]原
= -8

13 - 5 的运算过程为:

1
2
3
4
5
6
7
13 - 5 = 13 + (-5)
= [0000 0000 0000 1101]原 + [1000 0000 0000 0101]原
= [0000 0000 0000 1101]反 + [1111 1111 1111 1010]反
= [1 0000 0000 0000 0111]反
= [0000 0000 0000 0111]反
= [0000 0000 0000 0111]原
= 7

这足以证明,刚才的猜想是正确的:小数减去大数不会有问题,而大数减去小数的就不对了,结果始终相差 1 。

相差的这个 1 要进行纠正,但是又不能影响小数减去大数,怎么办呢?于是人们又绞尽脑汁设计出了补码,给反码打了一个 “补丁”,终于把相差的 1 给纠正过来了。

下面演示了按照补码计算的过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
6 - 18 = 6 + (-18)
= [0000 0000 0000 0110]补 + [1111 1111 1110 1110]补
= [1111 1111 1111 0100]补
= [1111 1111 1111 0011]反
= [1000 0000 0000 1100]原
= -12

18 - 6 = 18 + (-6)
= [0000 0000 0001 0010]补 + [1111 1111 1111 1010]补
= [1 0000 0000 0000 1100]补
= [0000 0000 0000 1100]补
= [0000 0000 0000 1100]反
= [0000 0000 0000 1100]原
= 12

5 - 13 = 5 + (-13)
= [0000 0000 0000 0101]补 + [1111 1111 1111 0011]补
= [1111 1111 1111 1000]补
= [1000 1111 1111 0111]反
= [1000 0000 0000 1000]原
= -8

13 - 5 = 13 + (-5)
= [0000 0000 0000 1101]补 + [1111 1111 1111 1011]补
= [1 0000 0000 0000 1000]补
= [0000 0000 0000 1000]补
= [0000 0000 0000 1000]反
= [0000 0000 0000 1000]原
= 8

你看,采用补码的形式正好把相差的 1 纠正过来,也没有影响到小数减去大数,这个 “补丁” 真是巧妙。

小数减去大数,结果为负数,之前(负数从反码转换为补码要加 1 )加上的 1,后来(负数从补码转换为反码要减 1 )还要减去,正好抵消掉,所以不会受影响。

而大数减去小数,结果为正数,之前(负数从反码转换为补码要加 1 )加上的 1,后来(正数的补码和反码相同,从补码转换为反码不用减 1 )就没有再减去,不能抵消掉,这就相当于给计算结果多加了一个 1 。

位运算

所谓位运算,就是对一个比特(Bit)位进行操作。比特(Bit)是一个电子元器件,8 个比特构成一个字节(Byte),它已经是粒度最小的可操作单元了。

C 语言提供了六种位运算符:

运算符 & | ^ ~ << >>
说明 按位与 按位或 按位异或 取反 左移 右移

按位与运算( & )

一个比特(Bit)位只有 0 和 1 两个取值,只有参与 & 运算的两个位都为 1 时,结果才为 1,否则为 0。例如 1 & 1 为 1,0 & 0 为 0,1 & 0 也为 0,这和逻辑运算符 && 非常类似 。

C 语言中不能直接使用二进制,& 两边的操作数可以是十进制、八进制、十六进制,它们在内存中最终都是以二进制形式存储,& 就是对这些内存中的二进制位进行运算。其他的位运算符也是相同的道理。

例如,9 & 5 可以转换成如下的运算(这里的整数皆为 C 语言中的 int 类型,即 4 字节 32 位):

1
2
3
4
  0000 0000 -- 0000 0000 -- 0000 0000 -- 0000 1001  ( 9 在内存中的存储 )
& 0000 0000 -- 0000 0000 -- 0000 0000 -- 0000 0101 ( 5 在内存中的存储 )
-----------------------------------------------------------------------------------
0000 0000 -- 0000 0000 -- 0000 0000 -- 0000 0001 ( 1 在内存中的存储 )

也就是说,按位与运算会对参与运算的两个数的所有二进制位进行 & 运算,9 & 5 的结果为 1 。

又如,-9 & 5 可以转换成如下的运算:

1
2
3
4
  1111 1111 -- 1111 1111 -- 1111 1111 -- 1111 0111  ( -9 在内存中的存储 )
& 0000 0000 -- 0000 0000 -- 0000 0000 -- 0000 0101 ( 5 在内存中的存储 )
-----------------------------------------------------------------------------------
0000 0000 -- 0000 0000 -- 0000 0000 -- 0000 0101 ( 5 在内存中的存储 )

-9 & 5 的结果是 5 。

再强调一遍,& 是根据内存中的二进制位进行运算的,而不是数据的二进制形式;其他位运算符也一样。以 -9 & 5 为例,-9 的在内存中的存储和 -9 的二进制形式截然不同:

1
2
 1111 1111 -- 1111 1111 -- 1111 1111 -- 1111 0111  ( -9 在内存中的存储 )
-0000 0000 -- 0000 0000 -- 0000 0000 -- 0000 1001 ( -9 的二进制形式,前面多余的 0 可以抹掉 )

按位与运算通常用来对某些位清 0,或者保留某些位。例如要把 n 的高 16 位清 0 ,保留低 16 位,可以进行 n & 0XFFFF 运算( 0XFFFF 在内存中的存储形式为 0000 0000 -- 0000 0000 -- 1111 1111 -- 1111 1111 )。

按位或运算( | )

参与 | 运算的两个二进制位有一个为 1 时,结果就为 1,两个都为 0 时结果才为 0 。例如 1 | 1 为 1 ,0 | 0 为 0 ,1 | 0 为 1,这和逻辑运算中的 || 非常类似。

例如,9 | 5 可以转换成如下的运算:

1
2
3
4
    0000 0000 -- 0000 0000 -- 0000 0000 -- 0000 1001  ( 9 在内存中的存储 )
| 0000 0000 -- 0000 0000 -- 0000 0000 -- 0000 0101 ( 5 在内存中的存储 )
-----------------------------------------------------------------------------------
0000 0000 -- 0000 0000 -- 0000 0000 -- 0000 1101 ( 13 在内存中的存储 )

9 | 5 的结果为 13 。

又如,-9 | 5 可以转换成如下的运算:

1
2
3
4
    1111 1111 -- 1111 1111 -- 1111 1111 -- 1111 0111  ( -9 在内存中的存储 )
| 0000 0000 -- 0000 0000 -- 0000 0000 -- 0000 0101 ( 5 在内存中的存储 )
-----------------------------------------------------------------------------------
1111 1111 -- 1111 1111 -- 1111 1111 -- 1111 0111 ( -9 在内存中的存储 )

-9 | 5 的结果是 -9 。

按位或运算可以用来将某些位置 1,或者保留某些位。例如要把 n 的高 16 位置 1,保留低 16 位,可以进行 n | 0XFFFF0000 运算( 0XFFFF0000 在内存中的存储形式为 1111 1111 -- 1111 1111 -- 0000 0000 -- 0000 0000 )。

按位异或运算( ^ )

参与 ^ 运算两个二进制位不同时,结果为 1,相同时结果为 0 。例如 0 ^ 1 为 1 ,0 ^ 0 为 0 ,1 ^ 1 为 0 。

例如,9 ^ 5 可以转换成如下的运算:

1
2
3
4
   0000 0000 -- 0000 0000 -- 0000 0000 -- 0000 1001  ( 9 在内存中的存储 )
^ 0000 0000 -- 0000 0000 -- 0000 0000 -- 0000 0101 ( 5 在内存中的存储 )
-----------------------------------------------------------------------------------
0000 0000 -- 0000 0000 -- 0000 0000 -- 0000 1100 ( 12 在内存中的存储 )

9 ^ 5 的结果为 12 。

又如,-9 ^ 5 可以转换成如下的运算:

1
2
3
4
   1111 1111 -- 1111 1111 -- 1111 1111 -- 1111 0111  ( -9 在内存中的存储 )
^ 0000 0000 -- 0000 0000 -- 0000 0000 -- 0000 0101 ( 5 在内存中的存储 )
-----------------------------------------------------------------------------------
1111 1111 -- 1111 1111 -- 1111 1111 -- 1111 0010 ( -14 在内存中的存储 )

-9 ^ 5 的结果是 -14 。

按位异或运算可以用来将某些二进制位反转。例如要把 n 的高 16 位反转,保留低 16 位,可以进行 n ^ 0XFFFF0000 运算( 0XFFFF0000 在内存中的存储形式为 1111 1111 -- 1111 1111 -- 0000 0000 -- 0000 0000 )。

取反运算( ~ )

取反运算符 ~ 为单目运算符,右结合性,作用是对参与运算的二进制位取反。例如 ~1 为 0 ,~0 为 1 ,这和逻辑运算中的 ! 非常类似。

例如,~9 可以转换为如下的运算:

1
2
3
~ 0000 0000 -- 0000 0000 -- 0000 0000 -- 0000 1001  ( 9 在内存中的存储 )
-----------------------------------------------------------------------------------
1111 1111 -- 1111 1111 -- 1111 1111 -- 1111 0110 ( -10 在内存中的存储 )

所以 ~9 的结果为 -10 。

例如,~-9 可以转换为如下的运算:

1
2
3
~ 1111 1111 -- 1111 1111 -- 1111 1111 -- 1111 0111  ( -9 在内存中的存储 )
-----------------------------------------------------------------------------------
0000 0000 -- 0000 0000 -- 0000 0000 -- 0000 1000 ( 9 在内存中的存储 )

所以 ~-9 的结果为 8 。

左移运算( << )

左移运算符 << 用来把操作数的各个二进制位全部左移若干位,高位丢弃,低位补 0 。

例如,9 << 3 可以转换为如下的运算:

1
2
3
<< 0000 0000 -- 0000 0000 -- 0000 0000 -- 0000 1001  ( 9 在内存中的存储 )
-----------------------------------------------------------------------------------
0000 0000 -- 0000 0000 -- 0000 0000 -- 0100 1000 ( 72 在内存中的存储 )

所以 9 << 3 的结果为 72 。

又如,(-9) << 3 可以转换为如下的运算:

1
2
3
<< 1111 1111 -- 1111 1111 -- 1111 1111 -- 1111 0111  ( -9 在内存中的存储 )
-----------------------------------------------------------------------------------
1111 1111 -- 1111 1111 -- 1111 1111 -- 1011 1000 ( -72 在内存中的存储 )

所以 (-9) << 3 的结果为 -72 。

如果数据较小,被丢弃的高位不包含 1,那么左移 n 位相当于乘以 2 的 n 次方。

右移运算( >> )

右移运算符 >> 用来把操作数的各个二进制位全部右移若干位,低位丢弃,高位补 0 或 1 。如果数据的最高位是 0 ,那么就补 0 ;如果最高位是 1 ,那么就补 1 。

例如,9 >> 3 可以转换为如下的运算:

1
2
3
>> 0000 0000 -- 0000 0000 -- 0000 0000 -- 0000 1001  ( 9 在内存中的存储 )
-----------------------------------------------------------------------------------
0000 0000 -- 0000 0000 -- 0000 0000 -- 0000 0001 ( 1 在内存中的存储 )

所以 9 >> 3 的结果为 1 。

又如,(-9) >> 3 可以转换为如下的运算:

1
2
3
>> 1111 1111 -- 1111 1111 -- 1111 1111 -- 1111 0111  ( -9 在内存中的存储 )
-----------------------------------------------------------------------------------
1111 1111 -- 1111 1111 -- 1111 1111 -- 1111 1110 ( -2 在内存中的存储 )

所以 (-9) >> 3 的结果为 -2 。

如果被丢弃的低位不包含 1,那么右移 n 位相当于除以 2 的 n 次方( 但被移除的位中经常会包含 1 )。

其他 - 字节顺序

字节顺序,指存储器中或在数字通信链路中,组成多字节的字的字节的排列顺序。

字节的排列方式有两个通用规则。例如,一个多位的整数,按照存储地址从低到高排序的字节中,如果该整数的最低有效字节(类似于最低有效位)在最高有效字节的前面,则称小端序;反之则称大端序

具体内容请参考维基百科。

参考