Intro
本文中数字的右下角标识代表该数字所使用的进制
计算机里的数据是什么?
首先要明确一点,计算机中几乎所有的信息,在实际上都是电信号。
这引申出了计算机所能控制的信息的最小单位——bit。
bit就是binary digit,当然计算机里的信息不是一串0和1,而是高电平与低电平,它们被表示为0和1。
这也就是说,下文所提到的所有的二进制数序列,在事实上是储存在计算机的储存元件中的一系列由高低电平组成的电信号序列。它们不是一个数,而是一系列的电信号。
高低电平事实上是一个范围,而非一个定值,这给了整个电路容忍噪声的能力。
选择了更高进制的计算机(如三进制)能在一个bit内表示更多的信息,但增加新的状态将不可避免的带来硬件成本的上升,选择成本还是信息量,是现实需要考虑的问题。
从数学的角度出发,事实上选择何种进制并无影响,数值并不会因为进制不同而变得不同。
于是我们现在知道了,计算机里的信号,都可以被表示为一系列的0和1。
更进一步,我们将n个bit组合成一串有序的数字串,我们就可以用这样的数字串去表示几乎所有的符号。
譬如ASCII。
我们使用8个bit来表示一个ASCII符号A
:
0100 0001 -> A
像这样将字符映射到二进制格式的过程就是编码。
而这样的一个8bit的序列,我们就称它是1byte,也就是一字节。
ASCII一共只有127个符号,只使用7bit就能完成编码,为什么要用8bit来编码?或者说,为什么1byte=8bit?
这其实是一个马屁股决定火箭直径的问题
事实上,在字节长度未被确定时,是有各种字节的长度不同的机器的。1byte = 5bit、6bit、7bit的机器一样是存在的。但或许是由于IBM System/360的成功,它所使用的1byte = 8bit被广泛接受,成为了标准。
字节是计算机储存与处理信息的基本单位。
进制互换
二进制数序列是计算机所能直接使用的,但一个过长的二进制数序列对于人类来说是难以识别的,因此,我们不得不引入识别负荷更小的记录方式。
在这里,我们姑且将二进制数序列看作是一个二进制数字,以方便进行进制的转换。
进制是进位计数制的意思,X进制就是指计数到X时就进一位。
以我们最常用的十进制来举例,当我们从零开始数,数到十的时候就往前进一位。由于我们最常用的进制就是十进制,这使得我们几乎认为一个数值的十进制表示就是这个数值本身。
上文中已经提到了,一个数值不因为其表达形式的改变而改变,所以接下来要注意,所有的转换,事实上只是同一个数值的不同表达形式。
由此我们来理解二进制。
以数字\(42\)来举例:
它的十进制表达是: \(42_{10} = (4 \times 10^1 + 2 \times 10^0)_{10}\)
它的二进制表达是: \(10 1010_{2} = (1 \times 2^5 + 0 \times 2^4 + 1 \times 2^3 + 0 \times 2^2 + 1 \times 2^1 + 0 \times 2^0)_{10}\)
你可以发现,对于一个有\(m\)位的\(n\)进制数字,它的十进制表达事实上就是它的第\(i(i \in [1,m])\)位上的数字\(x_i\)去乘\(n^{i-1}\)的累加。即:
\[\sum_{i=1}^{m} x_i \times n^{i-1} \]于是现在,你学会了将一个任意进制的数字转化为十进制表达。
同样的,你只需要将这个过程逆过来,就能将任意一个十进制数字转化为任意进制的表达。
让我们回到本部分开头所述的问题:一种识别负荷更小的二进制数序列记录方法——十六进制。
十六进制是计数到十六(十进制表达下)时就进一位。
即:\(0,1,2,3,4,5,6,7,8,9,A,B,C,D,E,F,10\)。
我们在上面已经知道,二进制数序列可以编码任意符号,当让也包括这十六个符号。
\(16 = 2^4\),这意味着这些符号只需要四位的二进制数序列就能完全编码。
于是,我们将0000
编码为十六进制的0
,0001
编码为十六进制的1
,依此类推,将十六个符号全部编码成四位的二进制数序列。这样,我们就得到了一个十六进制符号对应四位二进制数序列的映射表。
接下来就可以简易地表达一个长的二进制数序列了。我们将一个二进制数序列从最左边起,每四位做一个分割,将一个二进制数序列分割为多个四位长度的二进制数,如果最后一个不足四位就在它的右边补零。之后,我们将每一个四位二进制数序列按照刚刚得到的映射表写成十六位符号,于是,我们就将一个长二进制数序列改写成了一个短的十六进制数序列。
还是以42
举例:
它的二进制数序列表达为101010
,可以写作0010 1010
,即2 A
八进制也是同理的,不过由于\(8 = 2^3\),所以它仅需要三位二进制数序列就能完成编码。
它对101010
的分割是101 010
,也就是5 2
值得注意的是,这种方法对于数值来说同样是成立的,也就是说,对于一个二进制数,同样可以用这种方法来进行进制转换。
整数
我们已经知道了,我们所见到的数值,对于计算机来说事实上只是一串电信号。
但我们也知道计算机是可以进行计算的,而且计算能力很强,这意味着,没有智能并不代表你不能完成计算。
事实上,我们并非没有数值,我们可以用二进制数序列来编码数值。
只要我们将一个二进制数序列视作是一个二进制数,这个序列就能表达一个具体的数值。
于是我们有了无符号整数。它能表达的数字范围是\([0,2^{n}-1]\),\(n\)为编码该整数所使用的二进制序列长度。
同样的我们可以为它加上符号,我们将编码这个整数所使用的二进制序列的最左边那一位定义为\(-2^n\),这样,他能表达的数字范围就变成了\([-2^n,2^{n-1}-1]\)。
这种表达数字的方法便是补码。
事实上,有符号整数还有不同的表达方法,例如反码与原码,但它们都会引入\(\plusmn 0\)的问题。
我们现在知道了如何编码一个整数,那么这个编码的二进制数序列长度\(n\)应该是多少呢?
这与具体机器上不同语言的具体实现有关,但这个数值一般是\(8^n\),这是因为一个字节能表示八位的二进制数序列,而将这个数值设置成不同的字节将有利于储存与使用。
浮点
我们已经知道了使用一个二进制数序列可以简单准确地表示一个整数,但当这个问题扩展到小数时,一切就都变得复杂了起来。
根据上面我们已经知道,一个\(n\)进制的整数,它的数值可以用累加的方法求出,事实上,小数也是可以的。
我们将刚刚的公式稍作改写,对于任意一个数:
\[(x_m...x_1x_0.x_{-1}x_{-2}...x_{-n})_k \]它的值表达为十进制形式可以用如下公式求出:
\[\sum_{i={-n}}^{i={m}} x_i \times k^{i} \]这样,我们就表示出了任意进制下的小数。
那为什么说问题变得复杂了呢?
例如使用十进制小数无法在有限长度的编码内准确表示\(\frac{1}{3}\),不同进制下的小数表达出来的事实上是一个\(\frac{x}{k^n}(x,k,n \in N^+)\)它的分母是被固定的,这导致我们在有限长度编码下只能准确的表示出能被写为\(x_i \times k^i\)的数字,而其他的数字只能近似地去表示。
这也就是为什么常能听到浮点数是不准确的的说法。
IEEE 754
IEEE 754提供了一种简单地表示一个极大/极小的浮点数的方法。
事实上就是一种二进制下的科学计数法。
我们以\(2.5\)为例:
它的二进制表达是\(10.1\),那么将它写成二进制下的科学计数法就是\(1.01 \times (2^1)_{10}\)
如何将这个数字用二进制序数列编码出来?
IEEE使用4byte/8byte来表示一个单精度/双精度的浮点数。
它将它分为三个字段,分别是:
4byte:[31->s][30:23->exp][22:0->frac]
8byte:[63->s][62:52->exp][51:0->frac]
其中数字代表该字段开头及结尾的地址;s
是符号位,exp
是阶码,frac
是小数字段。
怎么使用这三个字段来表示一个小数?
首先我们将一个二进制小数写为以下式子的形式:
s
作为符号位,直接等效于式子中的s,即1是负、0是正。
exp
是阶码,它是一个以偏置形式表示的无符号整数,与\(E\)的关系是:\(exp = E + bias\),这里的\(bias\)是偏置量,它等于\((2^{n-1})_{10}\)这里的\(n\)是字段exp
的位数。使用这种表述形式是方便负指数的表达。
frac
是表达的数字的小数的部分,在科学计数法中,小数点前有且仅有非零的一位数,在二进制中这个数只能是\(1\),故可以省略。它直接编码\(M\)的小数部分,如果位数不足则在最后补零。
这样,我们就能表达一个规格化的值的浮点数了。
让我们看回刚刚的\(2.5\),我们先将它写成\((-1)^0 \times 1.01 \times (2^{1})_{10}\)
于是我们就能将它编码为单浮点0|100 0000 0|010 0000 0000 0000 0000 0000
这里用|
分隔开的就是对应的三个字段。
上面我们注意到,我说这个数是一个规格化的值。那么什么是规格化的值?
事实上,这种浮点数的表示一共有三种情况,由exp
与frac
来决定处于何种情况。
- 当
exp
既不为全0
,也不为全1
时,我们认为它是一个规格化的值。 - 当
exp
全为0
的时候,我们就认为这个值是一个非规格化的值 - 当
exp
全为1
的时候,我们认为它是一个特殊值。此时,若frac
为0
,我们认为它是无穷大;若frac
不为0
,我们则认为它不是一个数,即NaN
。
为什么要这样做?
例如,假设我想要表示\(0\),我们会发现,\(0\)无法写成科学计数法的形式,因为此时的\(E\)无法确定。
在一个非规格化的值的浮点数表示中,\(E = 1 - bias\)。此时我们已经不认为这个数是一个科学计数法表示的数字了,即我们不再认为这个数隐含一个小数点前为1的前提。
也就是说,这个时候我们认为\(M \in \left[ 0,1 \right)\)。当然frac
仍旧表示\(M\)的尾数部分。你可以简单的看作规格化数前隐含的1
变成了0
。
于是我们就能表示0,以及距离0非常近的数字了。
当然我们发现这些更改与符号位s
无关,也就是说,存在\(\plusmn 0\)。
当我们从零开始数exp
全为1的时候,这个浮点数就应该被解释为一个特殊的值了,它们被用于处理一些溢出或是无法计算的数值。
Outro
于是,我们顺利地将计算机所能识别的电信号转化成了人类所能识别的数据。
下一章我们便可以开始处理这些数据了。