计算机数值编码
计算机数值编码是指电子计算机内部表示数字的方式[1]。通常数值会储存为字节序列,而以组成它们的位元数给以名称,例如32位整数、64位浮点数。对于计算机处理器指令集所使用的位元编码,通常需要将其格式再转换以供外界的使用者来判读,例如打印和显示。对于纸笔书写看似相同的文字符号或数值,在不同类型的电子计算机处理器内部中,可能有不同的约定用于整数和实数。大多数计算都是使用适合处理器暂存器的数码格式来执行,但是某些软件系统允许使用多种储存方式,来表示任意大数。
二进制表示法
电脑会将资料用一串二进制的数字来表示,二进制数字是由位元所组成,也会将字节合成较大的单位,像是字节。
二进制数值 | 二进制数值 |
---|---|
000 | 0 |
001 | 1 |
010 | 2 |
011 | 3 |
100 | 4 |
101 | 5 |
110 | 6 |
111 | 7 |
二进制数字的长度(b) | 可能值的个数(N) |
---|---|
1 | 2 |
2 | 4 |
3 | 8 |
4 | 16 |
5 | 32 |
6 | 64 |
7 | 128 |
8 | 256 |
9 | 512 |
10 | 1024 |
... | |
计算机的基本储存单位是“位元”(bit),通过开关变化设置表达值0或1。位元的概念可以表示为1或0、开或关、是或不是、真或假等等。
一个位元可以表示二个值,而几个位元可以表示较大的数值。例如,三个位元就可以表示八个不同的数值,如表1所示。位元数越多,可以表达的数字也就越多,可以表达的数字会随着位元数指数成长, n位数的二进制数字可以表达2n种可能的数字,如表2所示。
为了方便理解,会将特定个数的二进制数字组合起来,变成较大的单位,且有其名称。
字节(byte)是长度可以表示字符的二进制位数组。在现有大部分的电脑中,字节是由8个位元所组成,由于字节的定义和表示字符所需要的位元个数有关,有些较旧的电脑可能有不同的字节定义[2]。在许多计算机系统结构中,字节是最小的可定址单元,也就是位址定址的最小单位。例如,即使64位元处理器一次可以处理64位元,仍然将其记忆体以8位元为单位来分割。这就是字节定址的记忆体。以往有许多CPU是以8位元的整数倍来读取资料[3]。字节由8个位元所组成的情形非常常见,但不是标准化的定义,因此有出现八位元(octet)的名称,明确说明是由8个位元所组成的单位。
nibble是由4个位元所组成的单位[4]。4个位元可以表示16个值,因此nibble也会称为十六进制数字[5]。
八进制和十六进制数
对二进制数字的表示方法。计算机通常使用二进制来表达数据,但是在实际中如果使用像这样的二进制:
- 1001 0011 0101 0001
这些很长的数字对于人手计算及观看上会很容易出错。因此计算机会使用八进制或更常使用的十六进制的,一个基于二进制的表达方式。
这个简单的解决方法十分有效,在平常的十进制体系中,有10数字(0到9)按以下方式构成排列:
- 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 ...
在八进制中,只有八个数字(0到7)按以下方式构成排列:
- 0 1 2 3 4 5 6 7 10 11 12 13 14 15 16 17 20 21 22 23 24 25 26 ...
也即是说,八进制的“10”相当于十进制的“8”,八进制的“20”相当于十进制的“16”,以此类推。
在十六进制中,只有十六个数字(0到9,然后是从a到f)按以下方式构成排列:
- 0 1 2 3 4 5 6 7 8 9 a b c d e f 10 11 12 13 14 15 16 ...
也即是说,十六进制的“10”相当于十进制的“16”,十六进制的“20”相当于十进制的“32”。
这些表示数值的方法都是表位置的系统,但是它们使用的不是像十进制那样的10,而是分别使用8和16。例如:
- 八进制756
- = 7 × 82 + 5 × 81 + 6 × 80
- = 7 × 64 + 5 × 8 + 6 × 1
- = 448 + 40 + 6 = 十进制494
- 十六进制3b2
- = 3 × 162 + 11 × 161 + 2 × 160
- = 3 × 256 + 11 × 16 + 2 × 1
- = 768 + 176 + 2 = 十进制946
于八进制来说,他们刚好和3位元二进制有一个完美的对应关系:
- 000 = 八进制0
- 001 = 八进制1
- 010 = 八进制2
- 011 = 八进制3
- 100 = 八进制4
- 101 = 八进制5
- 110 = 八进制6
- 111 = 八进制7
相似地,一个十六进制数刚好和一个4位元的二进制数对应。
0000 = 十六进制0 1000 = 十六进制8 0001 = 十六进制1 1001 = 十六进制9 0010 = 十六进制2 1010 = 十六进制a 0011 = 十六进制3 1011 = 十六进制b 0100 = 十六进制4 1100 = 十六进制c 0101 = 十六进制5 1101 = 十六进制d 0110 = 十六进制6 1110 = 十六进制e 0111 = 十六进制7 1111 = 十六进制f
因此,把一个很长的二进制数转换成一个八进制就变得很简便,例如:把二进制1001001101010001转化成八进制:
1 001 001 101 010 001 二进制 = 111521 八进制
转换成十六进制则是:
1001 0011 0101 0001 = 9351 十六进制
因为要把它转换成十进制(37,713)比较复杂,八进制和十六进制使得转换二进制机器级的数字变得简单方便。
有符号整数和补码
在定义负数中,有两种普遍的方法,分别是“无符号整数”及“有符号整数”。
无符号整数
这个方法保留了一个位来表示数值的符号。这个“符号位”可以位于数值的最左边,也可以位于数值的最右边。如果这个符号位为0,表示数值是正的,如果这个符号位为1,表示数值是负数。然而这样做对于计算机来说有可能带来一些缺点。这种编码使得0可以有正负两种,有符号整数会浪费掉一个表达数值的范围
有符号整数
更优胜的表达方式是对给定的位数的二进制数按其范围分成两半,其中前一半用来表示负数。例如,在4位数值中,你可以得到:
0000 = 十进制0 0001 = 十进制1 0010 = 十进制2 0011 = 十进制3 0100 = 十进制4 0101 = 十进制5 0110 = 十进制6 0111 = 十进制7 1000 = 十进制-8 1001 = 十进制-7 1010 = 十进制-6 1011 = 十进制-5 1100 = 十进制-4 1101 = 十进制-3 1110 = 十进制-2 1111 = 十进制-1
这个有符号整数的系统使用所知道的“补码”编码方式。
对16位元有符号数字编码来说,可以为从-32,768到32,767的数编码。
对一个32位元的有符号编码系统来说,可以为从-2,147,483,648到2,147,483,647的数编码。
与只改变符号位来表示负数的编码方式相比,“补码”编码方式与之有所不同。例如对于-5来说,只对符号位编码,应该是:
1101
但是对于“补码”编码方式来说,则是:
1011
这对于符号编码来说是-3。关于为什么计算机要使用补码这种编码方式会在后面解释。
所以,可以以二进制方式来表示正负两种不同的数值。请记住对于一个二进制数来说,只有两种解释方式。如果在内存中有一个这样的二进制数值:
1101
-- 这只能解释为十进制的“13”或“-5”。
定点小数
这种格式通常被用于商业计算(例如在电子表格或COBOL中);因为在这里,丢弃小数位来记录金钱是不能接受的。因此了解二进制如何存贮小数是十分有用的。
首先必须决定要用多少位元来存贮小数部分和多少位元来存储整数部分。假设使用32位元来表示这种格式,那么用16位元表示整数部分,16位元来表示小数部分。
小数部分可以沿用表示整数的方式:如果8位元接下来是4位元,是2位元,1位元,接下来就是半位元,1/4位元和1/8位元等等。
例如:
整数位 小数位 0.5 = 1/2 = 00000000 00000000.10000000 00000000 1.25 = 1 1/4 = 00000000 00000001.01000000 00000000 7.375 = 7 3/8 = 00000000 00000111.01100000 00000000
如果要表达1/5(十进制的0.2),那么不能得到精确的数值表达方式。最好的方法只能是:
- 13107/65536 = 00000000 00000000.00110011 00110011 = 0.1999969... 十进制
- 13108/65536 = 00000000 00000000.00110011 00110100 = 0.2000122... 十进制
一些小数使用二进制的方式不能精确地表达出来。除非使用一个特殊的办法。这个特殊的办法是分别使用两个数字来表达小数:一个是分子,一个是分母。然后可以使用学校学习的加、减、乘、除来得到它们。然而,这些方法不能表达更高级的数字(例如平方根),或者如果这两个分母的最小公倍数很大的话,那就难以使用。这就是使用定点小数表达小数的缺点。
浮点小数
当使用了有符号和无符号的数值表达方式时。如果遇到连32位元也不足以表达的大范围的数,或也许可以表达,但必须为此放弃小数位时,可以选择的以获得更大范围的数值的表达方式的方法是使用“浮点小数”格式而抛弃“定点小数”格式。
在十进制中,对以下的表达方式很熟悉:
- 1.1030402 × 105 = 1.1030402 × 100000 = 110304.02
或简写成:
1.1030402E5
这表示“1.103402乘以一个1后面跟着5个零的数”。可以得到一个确定的称谓“尾数”的数值(1.1030402),乘以一个10的某个幂级数(E5,表示105或100,000),也就是“幂级数”。如果使用一个负的幂级数,那就意味着乘以该正级数的倒数。例如:
- 2.3434E-6 = 2.3434 × 10-6 = 2.3434 × 0.000001 = 0.0000023434
使用这种定义的好处是可以得到更大范围的数值,虽然尾数部分的精确度受到影响。相似的原理可以应用于二进制中为计算机使用。人们设计了很多方式,但是作常用的是由美国电器和电子工程师协会定义的一种方法。它对64位浮点格式的定义是:
- 11位元二进制表示指数,使用“超1023”格式。这种格式使得指数可以表示为0到2047的无符号数,但是在得到真实值前必须先减去1023;
- 52位元尾数,使用无符号数字表示;
- 一个符号位;
下面使用一个例子来了解计算机如何使用内存的8字节存储这些数字:
第0字节: S x10 x9 x8 x7 x6 x5 x4 第1字节: x3 x2 x1 x0 m51 m50 m49 m48 第2字节: m47 m46 m45 m44 m43 m42 m41 m40 第3字节: m39 m38 m37 m36 m35 m34 m33 m32 第4字节: m31 m30 m29 m28 m27 m26 m25 m24 第5字节: m23 m22 m21 m20 m19 m18 m17 m16 第6字节: m15 m14 m13 m12 m11 m10 m9 m8 第7字节: m7 m6 m5 m4 m3 m2 m1 m0
其中的“S”表示符号位,“X”表示指数位(阶码?),“m”表示尾数位。一旦这些数字被读取,它将转换成:
- <符号> × (1 + <小数形式的尾数>) × 2<阶码> - 1023
这个方式提供的数字范围为:
最大值 | 最小值 | |
---|---|---|
正数 | 1.797693134862231E+308 | 4.940656458412465E-324 |
负数 | -4.940656458412465E-324 | -1.797693134862231E+308 |
这种方式也定义了一些不是数字的值,例如“NaNs”(“不是一个数字”)。这通常用来返回表示数字溢出的信息。一些程序使用32位元浮点小数。最普遍的是使用32位元尾数,1位元符号位,8位元阶码和“超127”格式。它提供7位十进制数字。
第0字节: S x7 x6 x5 x4 x3 x2 x1 第1字节: x0 m22 m21 m20 m19 m18 m17 m16 第2字节: m15 m14 m13 m12 m11 m10 m9 m8 第3字节: m7 m6 m5 m4 m3 m2 m1 m0
数字转换时使用:
- <符号> × (1 + <小数形式的尾数>) × 2<阶码> - 127
它的范围为:
最大值 | 最小值 | |
---|---|---|
正数 | 3.402823E+38 | 2.802597E-45 |
负数 | -2.802597E-45 | -3.402823E+38 |
通常可以把32位元的二进制浮点数称为“单精度”浮点数,而把64位元的二进制浮点数称为“双精度”浮点数。如果使用real的时候,通常表示双精度的浮点数;而使用float的时候,通常指单精度的浮点数。
但是要记住的是,浮点数在计算机的存储是连续的:当计算机内存中有一个64位元数据时,它可能是一个双精度的浮点数,也可能是两个单精度的浮点数,或4个有符号或无符号的整数,或其它8位的数据,这取决于计算机如何读取它们。如果计算机把4个无符号整数以双精度浮点小数方式读出的话,它可以得到一个双精度浮点小数,但这个数据可能是一个垃圾数据。
所以,现在虽然已经解决了正、负数的存贮方式,但是对于浮点数来说,仍然存在一些与整数一样的缺陷,例如:
- 像整数一样,浮点数也有范围。虽然得到了比整数要大得多的范围,但是仍然是有限的,如果试图把两个很大的数乘起来,可能会得到“数据上溢”的错误。而如果把一个小的数字除以一个函大的数字,那就可能使得指数的数值出错,出现“数据下溢”的错误。通常把最大值称为“机器无穷”,因为它是计算机所能处理的最大的数字。
- 另一个问题是精度。虽然有15位元的数字来表示很大的数,但是当对它进行四则运算的时候,它们可能不给任何提示就把一些数字丢弃。这意味着,如果把一个很小的数加到一个很大的数值上去的时候,由于这个数字太小,以至于15位元或16位元的精度都不能显示它,计算机就会把这个数字丢弃。如果在做计算的时候得到一个十分奇怪的数字,可能需要检查数据范围是否合适。
- 这意味着,如果做浮点计算,较小的数字很可能被丢弃了。虽然这在平常来说并不明显,但是如果在做要求很高的数学分析工作,这些错误可能会累积起来,以至于最后得到的结果十分不准确。
- 这个错误对进行数学研究的人来说十分重要,他们必须对误差十分了解,以及研究一些办法来减少误差,并且应该可以估计到误差的大小。
- 顺便说一句,“精度”问题与“范围”问题不同,前者指的是有关尾数的表示范围,后者指的是指数的表达范围。
- 另外一个不太明显的误差是由于浮点数的二进制和十进制并不完全相等。如果操作的数好是2的幂级数的倒数,例如0.75,那是用二进制可以准确的标示为0.11,因为它刚好是1/2+1/4的值。可是不幸的是,可能通常不会得到如此恰到好处的数字,这就是说,计算机会把一些数字丢掉,例如要表示0.1,只能使用无穷循环的二进制小数0.000110011……表示。
计算机不是万能的,它只是一部机器,并要符合一定的规则和受到一定的限制。在计算机虽好的解决方法下也有一些不可避免的不精确。
编程语言中的数
对于低级语言的编程者来说,他们要担心有符号和无符号、定点和浮点数的运算。他们必须使用十分不同的代码来实现操作。
但是,对高级语言的编程者来说,诸如LISP和Python提供了一些列诸如“有理数”、“复数”之类的抽象数据类型。而他们可以断言他们的系统可以使用数学操作做正确的运算。由于操作符重载,数学运算可以应用于任何数字——无论是有符号的、无符号的、有理数、定点小数、浮点小数或复数。
字符编码:ASCII和字符串
对于字符,例如姓名、地址或写给朋友的信,如果还记得几位是几位的话,当然可以使用数字来表达字符“A”或“?”或“Z”。计算机同理。计算机每次处理一个字节,所以使用单字节的数据来表达单个字符会很方便。可以使用这个:
0100 0110 (hex 46)
来表示字符“F”。计算机使用这样的“字符编码”来向显示程序传送要求的文本。
下面是一个用来存储西方字母的标准二进制编码,称为“美国信息交换标准码”(英文简称“ASCII”),下面的编码为ASCII编码,使用“d”表示十进制(decimal)编码,“h”表示十六进制(hexadecimal)代码,“o”表示八进制(octal)代码:
ASCII码表 ______________________________________________________________________ ch ctl d h o ch d h o ch d h o ch d h o ______________________________________________________________________ NUL ^@ 0 0 0 sp 32 20 40 @ 64 40 100 ' 96 60 140 SOH ^A 1 1 1 ! 33 21 41 A 65 41 101 a 97 61 141 STX ^B 2 2 2 " 34 22 42 B 66 42 102 b 98 62 142 ETX ^C 3 3 3 # 35 23 43 C 67 43 103 c 99 63 143 EOT ^D 4 4 4 $ 36 24 44 D 68 44 104 d 100 64 144 ENQ ^E 5 5 5 % 37 25 45 E 69 45 105 e 101 65 145 ACK ^F 6 6 6 & 38 26 46 F 70 46 106 f 102 66 146 BEL ^G 7 7 7 ` 39 27 47 G 71 47 107 g 103 67 147 BS ^H 8 8 10 ( 40 28 50 H 72 48 110 h 104 68 150 HT ^I 9 9 11 ) 41 29 51 I 73 49 111 i 105 69 151 LF ^J 10 a 12 * 42 2a 52 J 74 4a 112 j 106 6a 152 VT ^K 11 b 13 _ 43 2b 53 K 75 4b 113 k 107 6b 153 FF ^L 12 c 14 , 44 2c 54 L 76 4c 114 l 108 6c 154 CR ^M 13 d 15 _ 45 2d 55 M 77 4d 115 m 109 6d 155 SO ^N 14 e 16 . 46 2e 56 N 78 4e 116 n 110 6e 156 SI ^O 15 f 17 / 47 2f 57 O 79 4f 117 o 111 6f 157 DLE ^P 16 10 20 0 48 30 60 P 80 50 120 p 112 70 160 DC1 ^Q 17 11 21 1 49 31 61 Q 81 51 121 q 113 71 161 DC2 ^R 18 12 22 2 50 32 62 R 82 52 122 r 114 72 162 DC3 ^S 19 13 23 3 51 33 63 S 83 53 123 s 115 73 163 DC4 ^T 20 14 24 4 52 34 64 T 84 54 124 t 116 74 164 NAK ^U 21 15 25 5 53 35 65 U 85 55 125 u 117 75 165 SYN ^V 22 16 26 6 54 36 66 V 86 56 126 v 118 76 166 ETB ^W 23 17 27 7 55 37 67 W 87 57 127 w 119 77 167 CAN ^X 24 18 30 8 56 38 70 X 88 58 130 x 120 78 170 EM ^Y 25 19 31 9 57 39 71 Y 89 59 131 y 121 79 171 SUB ^Z 26 1a 32 : 58 3a 72 Z 90 5a 132 z 122 7a 172 ESC ^[ 27 1b 33 ; 59 3b 73 [ 91 5b 133 { 123 7b 173 FS ^\ 28 1c 34 < 60 3c 74 \ 92 5c 134 124 7c 174 GS ^] 29 1d 35 = 61 3d 75 ] 93 5d 135 } 125 7d 175 RS ^^ 30 1e 36 > 62 3e 76 ^ 94 5e 136 ~ 126 7e 176 US ^_ 31 1f 37 ? 63 3f 77 _ 95 5f 137 DEL 127 7f 177 ______________________________________________________________________
上面这个列表的最左边有一个字符,例如“FF”和“BS”,这些都不是文本字符。相反,它们是控制字符,当这些字符发送到特定的设备时,它将产生一些动作。例如“FF”表示换页,或弹出;“BS”表示退格,而“BEL”表示一个响声。在一个文本编辑器中,它们会显示成一个白色或黑色的方块,或笑脸、音符或其它替换符。要打出这些字符,可以使用CTRL键和一个合适的代码。例如同时按住“CTRL”和“G”,或简写成“CTRL-G”或“^G”可以打出一个BEL字符。
上面这个ASCII码表示定义了128个字符,这意味着ASCII码只需要7位元。但是,很多计算机都以字节为单位存储信息。这个额外的一位元在某些英语以外的国家被用于二个128个字集,一个“扩展”字集,例如EASCII
除了EASCII,还有很多不同的“扩展”字集,提供很多例如数学符号等的符号或非英语字符。这个扩展字集并没有进行标准化,并经常会引起混淆,同样的字串在不同的装置上显示不同的内容
这个表格强调了位元就是位元。这样的话,可以使用位来表示字符。可以把特殊的代码描述成特殊的十进制、八进制和十六进制,但是它们仍然是相同的代码。这些数值的表达,无论是十进制、八进制或十六进制,都只是相同的位的表达。
在一段话中表达很多的字符,例如:
- Tiger, tiger burning bright!
以ASCII码,会表示成:
- 54 69 67 65 72 2c 20 74 69 67 65 72 20 62 75 ...
计算机把这种ASCII“字符串”以连续空间的“数组”来存储。一些应用程序可以包括一个二进制数值表示字符串的长度,但是更通常的做法是使用一个表示结尾的字符NULL(ASCII表中的0字符〕表示字符串的结束。
参见
参考资料
- ^ Jon Stokes. Inside the machine: an illustrated introduction to microprocessors and computer architecture. No Starch Press. 2007: 66. ISBN 978-1-59327-104-6.
- ^ byte definition. [24 April 2012]. (原始内容存档于2017-04-03).
- ^ Microprocessor and CPU (Central Processing Unit). Network Dictionary. [1 May 2012]. (原始内容存档于3 October 2017).
- ^ nybble definition. [3 May 2012]. (原始内容存档于2016-10-11).
- ^ Nybble. TechTerms.com. [3 May 2012]. (原始内容存档于2019-03-13).