首页GitHub

二进制

二进制位现代计算机的基石,作为程序员,需要对二进制有一定的了解。

为什么计算机使用二进制?

计算机的逻辑电路通常只有开关的接通和断开两种状态。断开状态使用 0 表示,而接通状态使用 1 表示。由于每位数据只有接通跟断开两种状态,因此即便系统受到了一定的干扰,仍然能可靠地区分 0 和 1。但假设我们用十进制,每位数据有十种状态,如果系统受到了一定的干扰,那么是非常难以正确判断这十种状态的。与其它进制相比,二进制的数据具有抗干扰性强,可靠性高的优点。另外二进制也非常适合逻辑运算,逻辑运算中的真假,就可以通过二进制 0 和 1 来表示。

进制

我们日常最为熟悉的是十进制,十进制以 10 为基数,逢十进一,数位为 10n10^n 的形式,以 2871 为例:

2871=2×103+8×102+7×101+1×1002871 = 2 \times 10^3 + 8 \times 10^2 + 7 \times 10^1 + 1 \times 10^0

以同样的方式理解二进制,二进制则是以 2 为基数,进制的数位为 2n2^n 的形式。以二进制数字 110101 为例,将它转换为我们熟知的十进制的形式:

110101=1×24=5+1×24+0×23+1×22+0×21+1×20=53110101 = 1 \times 2^4=5 + 1 \times 2^4 + 0 \times 2^3 + 1 \times 2^2 + 0 \times 2^1 + 1 \times 2^0 = 53

同样八进制、十六进制也是如此,这里不再重复分析了。

进制转换

下面将使用 JavaScript 来实现进制的转换。在 JavaScript 中,无论是十进制转换成其他进制,还是其他进制转换成十进制,都可以通过内置的方法来实现。

在 JavaScript 中提供了 parseInt() 来实现其他进制转换为十进制,该函数第一个参数传数值字符串,第二个参数为基数:

// 十六进制转换为十进制
const num1 = parseInt("AF", 16); // 175

// 八进制转换为十进制
const num2 = parseInt("17", 8); // 15

// 二进制转换为十进制
const num3 = parseInt("10", 2); // 2

在 JavaScript 中,Number 对象使用 toString() 方法,则可以将十进制转换成其他进制:

// 十进制转换成二进制
const num1 = (3).toString(2); // 11

// 十进制转换成八进制
const num2 = (15).toString(8); // 17

// 十进制转换成十六进制
const num3 = (10).toString(16); // a

二进制位操作

对于二进制位操作,这里仍然以 JavaScript 来做代码演示。

在 JavaScript 中,所有的数值都是以 IEEE 754 64 位格式存储,但是位操作并不直接应用到 64 位表示,而是先把值转换为 32 位整数,再进行位操作,之后再把结果转换为 64 位。对开发者而言,就好像只有 32 位整数一样,因为 64 位整数存储格式是不可见的。既然知道了这些,就只需要考虑 32 位整数即可。

有符号整数使用 32 位的前 31 位表示整数值。第 32 位表示数值的符号,如 0 表示正,1 表示负,这一位成为符号位,它的值决定了数值其余部分的格式。正值以真正的二进制格式存储,即 31 位中的每一位都代表 2 的幂。如果一个位为空,则以 0 填充,相当于忽略不计。负值以一种称之为补码的二进制编码存储。

在对 JavaScript 中的数值应用位操作时,后台会发生转换:64 位数值会转换为 32 位数值,然后执行位操作,最后再把结果从 32 位转换为 64 位存储起来。整个过程就像处理了 32 位数值一样,这让二进制操作变得与其他语言中类似。但这个转换也导致了一个奇特的副作用,即特殊值 NaN 和 Infinity 在位操作中的会被当成 0 处理。

按位非

按位非操作符用 ~ 表示,它的作用是返回数值的反码。按位非的最终效果是对数值取反并减 1:

let num1 = 25; // 二进制00000000000000000000000000011001
let num2 = ~num1; // 二进制11111111111111111111111111100110
console.log(num2); // -26

按位与

按位与操作符用 & 表示,有两个操作数。本质上,按位与就是将两个数的每一个位对齐,然后基于真值表中的规则,对每一位执行相应的与操作。在两位都是 1 时返回 1,在任何一位是 0 时返回 0。

let result = 25 & 3;
console.log(result); // 1

二进制计算过程:

 25 = 0000 0000 0000 0000 0000 0000 0001 1001
  3 = 0000 0000 0000 0000 0000 0000 0000 0011
---------------------------------------------
AND = 0000 0000 0000 0000 0000 0000 0000 0001

按位或

按位或操作符用 | 表示,同样有两个操作数。按位或操作在至少一位是 1 时返回 1,两位都是 0 时返回 0。

let result = 25 | 3;
console.log(result); // 27

计算结果:

 25 = 0000 0000 0000 0000 0000 0000 0001 1001
  3 = 0000 0000 0000 0000 0000 0000 0000 0011
---------------------------------------------
 OR = 0000 0000 0000 0000 0000 0000 0001 1011

按位异或

按位异或用 ^ 表示。异或只有两位不相同的时候才返回 1。

let result = 25 ^ 3;
console.log(result); // 26

计算结果:

 25 = 0000 0000 0000 0000 0000 0000 0001 1001
  3 = 0000 0000 0000 0000 0000 0000 0000 0011
---------------------------------------------
XOR = 0000 0000 0000 0000 0000 0000 0001 1010

左移位

左移操作符用 << 表示,会按照指定的位数将数值的所有位向左移动。这里暂时不考虑左移数字溢出的情况。所谓的溢出,是指二进制的位数超出了系统所指定的位数。目前主流的系统都支持最少 32 位的整型数字,如果进行左移操作的二进制已经超过 32 位,左移后的数字就会溢出,需要将溢出的位数去除。

let oldValue = 2; // 等于二进制 10
let newValue = oldValue << 1; // 等于二进制 100,即十进制 4

需要注意的是,左移并不影响符号位,左移右端多出的空位,使用 0 来填充,即左移多少位,右端则填充多少个 0。另外根据上面的结果可以看出,二进制左移一位,实际上就是将数字翻倍。

右移位

右移操作,有符号右移(即 Java 中的)跟无符号右移(即 Java 中的逻辑右移)这两种情况有些区别:

  • 有符号右移操作符用 >> 表示,它实际上是左移的逆运算。当有符号右移时,左侧出现的空位,用符号位的值进行填补。
  • 无符号右移操作符用 >>> 表示,左侧出现的空位,用 0 填补。

参考资料