二进制位现代计算机的基石,作为程序员,需要对二进制有一定的了解。
计算机的逻辑电路通常只有开关的接通和断开两种状态。断开状态使用 0 表示,而接通状态使用 1 表示。由于每位数据只有接通跟断开两种状态,因此即便系统受到了一定的干扰,仍然能可靠地区分 0 和 1。但假设我们用十进制,每位数据有十种状态,如果系统受到了一定的干扰,那么是非常难以正确判断这十种状态的。与其它进制相比,二进制的数据具有抗干扰性强,可靠性高的优点。另外二进制也非常适合逻辑运算,逻辑运算中的真假,就可以通过二进制 0 和 1 来表示。
我们日常最为熟悉的是十进制,十进制以 10 为基数,逢十进一,数位为 的形式,以 2871 为例:
以同样的方式理解二进制,二进制则是以 2 为基数,进制的数位为 的形式。以二进制数字 110101 为例,将它转换为我们熟知的十进制的形式:
同样八进制、十六进制也是如此,这里不再重复分析了。
下面将使用 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 填补。