进制 1 代码 1 数字 1 探源 4

计算机中的数字

http://nozer0.github.io/zh/technology/system/numbers-in-computer/
2014-04-29 by nozer0

这次,我们来看一下数字在计算机中是如何存储的。

在开始前,让我们先来看一个简单程序。

#include <stdio.h>

int main(int argc, char* argv[]) {
	int i = 1;
	float *p2 = (float *)&i;
	printf("%.20g\n", *p2);
	printf("%d\n", 0.4 + 0.2 == 0.6);
	return 0;
}

有些意外?

进制

我想对于进制不需要更多解释了,列举一些计算机中经常涉及的进制。

Binary Octal Decimal Hexadecimal
10011101 235 157 9D
0.100111 0.47 0.609375 0.9C

如何在不同进制间转换?看个例子。

	Q:
		26(D) = ?(B)
	A:
	            26 | 13 | 6 | 3 | 1
	        /   2
	remainder    0 |  1 | 0 | 1 | 1
		26(D) = 11010(B)
	``````
	Q:
		0.8125(D) = ?(B)
	A:
	fractional  0.8125 | 0.625 | 0.25 | 0.5 | 0
	         *  2
	integer              1     | 1    | 0   | 1
		0.8125(D) = 0.1101(B)

数字码

计算机天然地使用二进制来表示数字,或者说,是用一系列的bit位来表示数字,通常用32位存储整数。

直接用数字的二进制表示再加上一bit表示符号位,0表示正数,1表示负数,这种方式被称为‘原码(original code)’表示法。原码最容易理解,但需要根据符号位不同来决定做加法还是减法,而且0有两种格式,‘0 0..0’以及‘1 0..0’。

为了简便计算,人们定义了另一种称为‘反码(anti code)’或‘一补码’的表示法,如果是负数,就将原码除符号位以外的所有bit反转,否则,与原码相同。符号位也可以直接参加运算,同时,可以用加法代替减法运算,比如。

	  1 1110111 (-8)             0 0001000 (+8)
	+ 0 0001101 (+13)          + 0 0001101 (+13)
	------------------          ------------------
	1 0 0000100                0 0 0010101
	+         1 (carry)        +         0 (carry)
	------------------          ------------------
	  0 0000101 (+5)             0 0010101 (+21)

不过,反码还是存在两种0的格式,+0的‘0 0..0’,还有-0的‘1 1..1’。‘二补码’,简称‘补码(complement code)’,成了最后的赢家,成为目前整数的标准表示。区别就是,负数表示为反码的负数再加1,其余相同,‘1 1..1’变为-1,而不是-0,相比反码,可以多表示一个‘-2^(bit-1)’的数字,也不需要最后再加上进位的步骤。

实际上,-8的补码111111000 = 244 和+8的补码000001000 = 8对于模256同余,这就是为什么可以只用简单的加法器和补码器来实现减法的原因。

	  1 1111000 (-8)             0 0001000 (+8)
	+ 0 0001101 (+13)          + 0 0001101 (+13)
	------------------          ------------------
	1 0 0000101 (+5)           0 0 0010101 (+21)

‘移码(shift code, bias code)’, 在下面介绍到的IEEE.754标准中有应用,是给数字本身的二进制值加上2^(bit-1)的位移来避免负数形式。

  正数(38) 负数(-38) 定义 优缺点
原码 0 0100110 1 0100110 符号 + 二进制 易理解,符号位不能参加运算
反码 0 0100110 1 1011001 X > 0 ? : ~b 符号位可加入运算,0有两种格式
补码 0 0100110 1 1011010 anti + 1 +0和-0统一表示,对32位来说可以表示128
移码 1 0100110 0 1011010 X + 2^(k-1) 大小比较很方便,0有唯一的格式
范围 UInteger Integer Fixed Fraction
原码 0 ~ 2^32-1 1-2^31 ~ 2^31-1 -1+2^(-31) ~ 1-2^(-31)
反码   1-2^31 ~ 2^31-1 -1+2^(-31) ~ 1-2^(-31)
补码   -2^31 ~ 2^31-1 -1 ~ 1-2^(-31)
移码   -2^31 ~ 2^31-1  

字节序

我们已经知道了不同的数字码表示,但当存储到字节中时,根据字节顺序的不同,还有两种不同的格式。如果把最高有效数字放在地址最低的字节,则成为‘大头(big-endian)’排序,反之,则是‘小头(little-endian)’排序,名称来源于《格列佛游记》一书。 例如,数字‘0xabcd’,如果是大头排序的话,存储的字节从低到高是从‘a’到‘d’,反之则是从‘d’到‘a’。目前绝大多数的CPU,像Intel、AMD都是使用小头排序。

浮点数

我们已经知道计算机如何用补码来存储整数,那小数呢?最简单的方法就是预先规定小数点的位置,因此称为‘定点’表示法,比如,如果我们定义小数点在第三bit,即0~2bit表示小数部分,则0 0010 110 = 2.75。

定点数表示法相对简单,但是可表示数字的范围比相同bit位的整数还要小,为了能表示更多的数字以及更大的范围,通常都用‘浮点’表示法来表示小数,也就是说,小数点是浮动的。

规格化

一个数字可以有无数的表示形式,例如,100110可以表示为110.11 * 2^3,或者1.1011 * 2^5,将数字转化为整数部分只有一位数字的表示方法,称为‘规格化(Normalization)’。

与‘规格化’相反,‘非规格化(Denormalization)’没有指数部分。

IEEE.754

所有现代的计算机都遵循IEEE 754标准来存储浮点数。格式表示如下。

类型 符号 指数 小数
float 32 1 8 23
double 64 1 11 52

由上表可见,浮点数由三部分组成,符号位(sign),指数部分(exponent)和小数部分(fraction),我们将使用小写字母‘s’,‘e’和‘f’来表示各个部分的bit长度,‘S’,‘E’和‘F’来表示值。

指数部分用移码表示,指数 = E - bias = E - (2^(e-1) - 1),对于32位,位移 = 2^(8-1) - 1 = 127,指数 = -127 ~ 128。

为表示更多的精度,同时应用了规格化及非规格化的形式。

规格化的数字可以有更多的有效位,而且,所有规格化数字整数部分都是1,因此可以默认省略,可得出公式float = -1^S * 1.F * 2^(E - (2^(e-1) - 1))

以32位为例子,如果我们只使用规格化数字,比0大的最小数字绝对值是1.0..00 * 2^-127,次之是1.0..01 * 2^-127,最小绝对值和0之间的空距比之与次小值的差距2^-(127+23) = 2^-150大的多。更直观的可以看下图。

       ┌─── 2^-149 ───┐ ┌ 2^-150 ┐                         ┌ 2^-150 ┐ ┌──────────── 2^-127 ────────────┐
1.0..1*2^-126, 1.0..0*2^-126, 1.1..1*2^-127, ..., 1.0..1*2^-127, 1.0..0*2^-127,                        0

这被称为‘突然下溢’,因此对于绝对值小于2^(e-1) - 2的数字,IEEE使用了非规格化的表示方式,并且指数的位移部分+1,来实现所谓的‘渐进下溢’。

       ┌─── 2^-148 ───┐                     ┌ 2^-149 ┐     ┌ 2^-149 ┐                       ┌ 2^-149 ┐
1.0..1*2^-125, 1.0..0*2^-125, ..., 1.0..1*2^-126, 1.0..0*2^-126, 0.1..1*2^-126, ..., 0.0..1*2^-126,  0

IEEE 754浮点表示的结构表。

  指数 E 小数 32 bits
0 00..0 2 - 2^(e-1) [0.]00..0 0
Min Denormalized N 00..0 2 - 2^(e-1) [0.]00..1 2^-149
Denormalized N 00..0 2 - 2^(e-1) [0.]xx..x 0.F * 2^-126
Max Denormalized N 00..0 2 - 2^(e-1) [0.]11..1 1 - 2^-149
Min Normalized N 00..1 2 - 2^(e-1) [1.]00..0 2^-126
Normalized N xx..x E + 1 - 2^(e-1) [1.]xx..x 1.F * 2^(E - 127)
Max Normalized N 11..0 2^(e-1) - 1 [1.]11..1 2^128-2^104
Infinity 11..1 1 - 2^(e-1) [1.]00..0  
NaN 11..1 1 - 2^(e-1)    

现在,让我们回头看看开头的例子。

综上,我们可以得出,1的整数字节形式为‘00000000 00000000 00000000 00000001’,同样的字节在浮点数中表示2^-149。

0.2 + 0.4的结果表示为字节‘00111111 00011001 10011001 10011010’,实际上,这就是0.6的浮点数表示,问题出在精度上,事实上,0.6的二进制表示是一个无限循环小数,‘0.100[1100]*’,是不可能被计算机精确表示的,根据不同精度,‘00000000 00000000 00000000 00000001’表示的结果可能是0.6000000000000001或0.60000000000000008,0.2和0.4同理。


可以用这个小工具来转换不同进制的数字及数字码表示。

进制 1 代码 1 数字 1 探源 4