计算机中的位运算
基础
- 位(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 | 6 - 18 = 6 + (-18) |
直接用原码表示整数,让符号位也参与运算,对于类似上面的减法来说,结果显然是不正确的。
于是人们开始继续探索,不断试错,后来设计出了反码。下面就演示了反码运算的过程:
1 | 6 - 18 = 6 + (-18) |
这样一来,计算结果就正确了。
然而,这样还不算万事大吉,我们不妨将减数和被减数交换一下位置,也就是计算 18 - 6 的结果:
1 | 18 - 6 = 18 + (-6) |
按照反码计算的结果是 11,而真实的结果应该是 12 才对,它们相差了 1 。
多出来的 1 是加法运算过程中的进位,它溢出了,内存无法容纳,所以直接删除。
6 - 18 的结果正确,18 - 6 的结果就不正确,相差 1。按照反码来计算,是不是小数减去大数正确,大数减去小数就不对了,始终相差 1 呢?我们不妨再看两个例子,分别是 5 - 13 和 13 - 5 。
5 - 13 的运算过程为:
1 | 5 - 13 = 5 + (-13) |
13 - 5 的运算过程为:
1 | 13 - 5 = 13 + (-5) |
这足以证明,刚才的猜想是正确的:小数减去大数不会有问题,而大数减去小数的就不对了,结果始终相差 1 。
相差的这个 1 要进行纠正,但是又不能影响小数减去大数,怎么办呢?于是人们又绞尽脑汁设计出了补码,给反码打了一个 “补丁”,终于把相差的 1 给纠正过来了。
下面演示了按照补码计算的过程:
1 | 6 - 18 = 6 + (-18) |
你看,采用补码的形式正好把相差的 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 | 0000 0000 -- 0000 0000 -- 0000 0000 -- 0000 1001 ( 9 在内存中的存储 ) |
也就是说,按位与运算会对参与运算的两个数的所有二进制位进行 & 运算,9 & 5 的结果为 1 。
又如,-9 & 5 可以转换成如下的运算:
1 | 1111 1111 -- 1111 1111 -- 1111 1111 -- 1111 0111 ( -9 在内存中的存储 ) |
-9 & 5 的结果是 5 。
再强调一遍,& 是根据内存中的二进制位进行运算的,而不是数据的二进制形式;其他位运算符也一样。以 -9 & 5 为例,-9 的在内存中的存储和 -9 的二进制形式截然不同:
1 | 1111 1111 -- 1111 1111 -- 1111 1111 -- 1111 0111 ( -9 在内存中的存储 ) |
按位与运算通常用来对某些位清 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 | 0000 0000 -- 0000 0000 -- 0000 0000 -- 0000 1001 ( 9 在内存中的存储 ) |
9 | 5 的结果为 13 。
又如,-9 | 5 可以转换成如下的运算:
1 | 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 | 0000 0000 -- 0000 0000 -- 0000 0000 -- 0000 1001 ( 9 在内存中的存储 ) |
9 ^ 5 的结果为 12 。
又如,-9 ^ 5 可以转换成如下的运算:
1 | 1111 1111 -- 1111 1111 -- 1111 1111 -- 1111 0111 ( -9 在内存中的存储 ) |
-9 ^ 5 的结果是 -14 。
按位异或运算可以用来将某些二进制位反转。例如要把 n 的高 16 位反转,保留低 16 位,可以进行 n ^ 0XFFFF0000 运算( 0XFFFF0000 在内存中的存储形式为 1111 1111 -- 1111 1111 -- 0000 0000 -- 0000 0000 )。
取反运算( ~ )
取反运算符 ~ 为单目运算符,右结合性,作用是对参与运算的二进制位取反。例如 ~1 为 0 ,~0 为 1 ,这和逻辑运算中的 ! 非常类似。
例如,~9 可以转换为如下的运算:
1 | ~ 0000 0000 -- 0000 0000 -- 0000 0000 -- 0000 1001 ( 9 在内存中的存储 ) |
所以 ~9 的结果为 -10 。
例如,~-9 可以转换为如下的运算:
1 | ~ 1111 1111 -- 1111 1111 -- 1111 1111 -- 1111 0111 ( -9 在内存中的存储 ) |
所以 ~-9 的结果为 8 。
左移运算( << )
左移运算符 << 用来把操作数的各个二进制位全部左移若干位,高位丢弃,低位补 0 。
例如,9 << 3 可以转换为如下的运算:
1 | << 0000 0000 -- 0000 0000 -- 0000 0000 -- 0000 1001 ( 9 在内存中的存储 ) |
所以 9 << 3 的结果为 72 。
又如,(-9) << 3 可以转换为如下的运算:
1 | << 1111 1111 -- 1111 1111 -- 1111 1111 -- 1111 0111 ( -9 在内存中的存储 ) |
所以 (-9) << 3 的结果为 -72 。
如果数据较小,被丢弃的高位不包含 1,那么左移 n 位相当于乘以 2 的 n 次方。
右移运算( >> )
右移运算符 >> 用来把操作数的各个二进制位全部右移若干位,低位丢弃,高位补 0 或 1 。如果数据的最高位是 0 ,那么就补 0 ;如果最高位是 1 ,那么就补 1 。
例如,9 >> 3 可以转换为如下的运算:
1 | >> 0000 0000 -- 0000 0000 -- 0000 0000 -- 0000 1001 ( 9 在内存中的存储 ) |
所以 9 >> 3 的结果为 1 。
又如,(-9) >> 3 可以转换为如下的运算:
1 | >> 1111 1111 -- 1111 1111 -- 1111 1111 -- 1111 0111 ( -9 在内存中的存储 ) |
所以 (-9) >> 3 的结果为 -2 。
如果被丢弃的低位不包含 1,那么右移 n 位相当于除以 2 的 n 次方( 但被移除的位中经常会包含 1 )。
其他 - 字节顺序
字节顺序,指存储器中或在数字通信链路中,组成多字节的字的字节的排列顺序。
字节的排列方式有两个通用规则。例如,一个多位的整数,按照存储地址从低到高排序的字节中,如果该整数的最低有效字节(类似于最低有效位)在最高有效字节的前面,则称小端序;反之则称大端序。
具体内容请参考维基百科。