二进制小数

就像十进制小数一样,二进制的小数点左边是正幂,右边是负幂,只不过幂底由10变成2。小数点可以移动,左移表示除以2,右移表示乘以2。

同十进制小数只能精确表示10的幂组成的数一样,二进制小数也只能精确表示由2的幂组成的数。同十进制小数一样,如果想要更接近不能精确表示的数,只有通过增加位数来逐渐近似。

2.45 二进制小数转换

小数值 二进制表示 十进制表示
1/8 0.001 0.125
3/4 0.11 0.75
25/16 1.1001 1.5625
43/16 10.1011 2.6875
9/8 1.001 1.125
47/8 101.111 5.875
51/16 11.0011 3.1875

2.46 导弹计时错误

  1. 0.1的二进制表示是 0.000110011[0011],程序中的x是 0.00011001100110011001100。将两者对齐之后互相减一下:
            0.00011001100110011001100110011001100110011.....
            0.00011001100110011001100
    

    得到结果是 0.00000000000000000000000[1100]

  2. 近似的10进制值是2的24次幂分之一加上2的25次幂分之一。
  3. 100个小时之后,系统计数的次数运行了100*3600*10 = 3600000次。误差大概是0.343秒。
  4. 2000*343 = 686米

IEEE浮点数表示

浮点数分为单精度和双精度。单精度32位,双精度64位。其构成是:

  1. 单精度: 1位符号位,8位指数位,23位尾数
  2. 双精度: 1位符号位,11位指数位,52位尾数

符号位如果=0,表示是一个正数,是1,表示是一个负数。

指数位是最关键的位置,这个部分的数值,决定了这个浮点数的三种情况。以单精度为例:

  1. 指数位的原始二进制数是00000001——11111110,即不等于无符号的的上下限0和255,这表示规格化数,可以理解成表示一般的小数。这种情况下,指数等于无符号数的大小减去偏置数,尾数的部分自动变成1.尾数。
  2. 指数位的原始二进制数是00000000,这个叫做非规格化数,此时尾数的部分就是.尾数,不再加上1。此时阶码固定等于1-偏置数。非规格化用来表示接近与0的数值。
  3. 指数位的原始二进制数是11111111,这个是特殊值。如果此时尾数全部为0,则表示0,有+0和-0之分。如果尾数不为0,表示NaN。

明白了小数的表示之后,就可以发现很有趣的现象,在都是正数的情况下,正好是按照无符号数逐渐增大来排列的。而负数排列的时候,就是按照降序来排列。

练习题2.47 练习IEEE浮点数

1个符号位,2个阶码位,2个尾数位的浮点数,偏置显然是1.

二进制 e 无符号数 E 减去偏置后的实际值 阶数实际值 f 表面尾数 M 实际尾数 2E*M 尾数实际值 V 实际分数值 十进制
0 00 00 0 0 1 0/4 0/4 0 0 0.0
0 00 01 0 0 1 1/4 1/4 1/4 1/4 0.25
0 00 10 0 0 1 1/2 1/2 1/2 1/2 0.5
0 00 11 0 0 1 3/4 3/4 3/4 3/4 0.75
0 01 00 1 0 1 0 1 1 1 1.0
0 01 01 1 0 1 1/4 5/4 5/4 5/4 1.25
0 01 10 1 0 1 1/2 3/2 3/2 3/2 1.5
0 01 11 1 0 1 3/4 7/4 7/4 7/4 1.75
0 10 00 2 1 2 0 1 2 2 2.0
0 10 01 2 1 2 1/4 5/4 5/2 5/2 2.5
0 10 10 2 1 2 1/2 3/2 3 3 3.0
0 10 11 2 1 2 3/4 7/4 7/2 7/2 3.5

浮点数在越靠近0的地方越密集,在远离0的地方变稀疏。

通过实际操练,可以知道常见的浮点数的最大和最小范围。比如单精度浮点数,E最低取到1-127 = -126,尾数部分最低就是1,则就是2的-23次方,合起来最小数字就是2的-149次方。

最大数字则是E取到254,指数=254-127 = 127,尾数部分全是1,此时代表1.(23个1) = 2 – 2的-23次方。双精度的也就可知了。

练习 2.48 解释练习2.6中的情况

0x00359141写成二进制是:0000 0000 0011 0101 1001 0001 0100 0001。

0x4A564504写成二进制是:0100 1010 0101 0110 0100 0101 0000 0100。

移动一下找匹配的部分:

  00000000001101011001000101000001
    01001010010101100100010100000100

现在就可以来解释这种匹配不是巧合的原因了。0x00359141的二进制,不补到32位的表示是11 0101 1001 0001 0100 0001。将其改成规格化数,则是1.1 0101 1001 0001 0100 0001

这个时候可以看到,小数点往右移动了21位,单精度情况下,阶码的值应该等于127+21 = 148,即二进制的10010100。尾数部分注意由于是非规格化数,开头的1可以去掉,剩下21位在末尾补2个0。按照顺序写出的浮点数是:

    0 10010100 1 0101 1001 0001 0100 0001 00

这就解释了为什么看起来像是整数的除了第一位1之外的部分被包含在浮点数中间一样。而且很显然,如果整数的二进制超过23位,精度就会有损失了。

2.49 不能精确由浮点数表示的正整数

从上边一个题目可以看出来,如果一个数字有N位二进制数,按照规格化分割之后第一位是1,后边有N-1位二进制表示,第一位1可以省略。而浮点数格式能存放n位,即 N-1 <= n。

即N<=n+1,所以当N = n+2的时候,如果尾数不是0,就会出现问题。因此最小的正整数,就是2的n+1次方再加1

可以用很简单的方法推导,如果n=4,则5位2进制数都可以放下,因为最高1位去掉之后,还有4位可以精确表示。但是到了6位二进制,如果是100000,依然可以精确表示,因为没有尾数。

但是一旦有最后一位,比如100001,写成1.00001*2的5次方的时候,最后一个1就会丢失,所以对于4位尾数的浮点数,100001 = 33就无法精确表示了。

对于单精度浮点数,2的24次方+1这个整数就无法表示了。这个数字就是16777216+1 = 16777217

舍入方向

这个其实就是对尾数的操作的不同方式。

默认的舍入方式是偶数舍入,也就是找最近的舍入。对于不在两个整数之前的浮点数,就是离其最近的整数。而对在两个数字正中间的整数,则按照让舍入位的最后一位为0来进行舍入。

2.50 练习:

  1. 10.010 ,由于这个数正好在要舍入的10.0的一半,所以直接舍入到10.0
  2. 10.011 ,这个数字直接舍入到10.1
  3. 10.110 ,这个数在要舍入的10.1的一半,所以要舍入到10.1的最低位是0,也就是11.0
  4. 11.001 ,这个数不是一半,直接舍入到11.0

2.51 练习:

已经知道x = 0.0001 1001 1001 1001 1001 100,如果要舍入到23位小数。那就先来多写几位看看:

0.0001 1001 1001 1001 1001 100 1100 1100 1,很显然结果是 0.0001 1001 1001 1001 1001 101,实际上这个数要比0.1要大

用这个数字减去0.1,由于是舍入到23位小数,那么结果就是23位小数之后的无限循环部分

2.52 练习:

格式A 格式B
011 0000 1 0111 000 1
101 1110 阶码 = 5 -3 = 2,结果是15/8*2的2次方 = 15/2 = 7.5 由于格式B的小数也是3位,可以放得下这三位,阶码也为2即可,所以是1001 111 7.5
010 1001 2-3 = -1 值是 (1+(1/2+1/16))/2 = 25/32 由于放不下这么多小数位,所以向0舍入,结果是0110 100 3/4
110 1111 8*(1+1/2+1/4+1/8+1/16) = 31/2 首先变换相同的阶码是10,则前四位是1010,如果不舍入 应该是1010 1111,然后舍入到1011 000 16
000 0001 这个是非规格化,所以阶码=1-3 = -2,结果是1/4*1/16 = 1/64 这个要注意,最小的是2的6次方分之1,而阶码4位可以直接表示出-6,所以就是0001 000 1/64

浮点运算

浮点加法有个最大的问题,就是不具有结合性,大数会把小的数字吃掉,这是因为舍入的原因。乘法也一样。

C语言在支持IEEE 754的机器上,float对应单精度浮点,double对应双精度浮点。此外在X86上,long double对应80位长度的浮点数,但这个不能移植。

所以很多时候,会直接定义浮点数。

2.53 完成下列定义

  1. #define POS_INFINITY,正无穷,通过分析可以知道,双精度正无穷最高位是0,阶码的11位全是1,然后是52位的0。写成十六进制就是0x7FF0000000000000
  2. #define NEG_INFINITY,负无穷,通过分析可以知道,12个1,然后是52位的0。写成十六进制就是0xFFF0000000000000
  3. #define NEG_ZERO,符号位是1,0是非规格化数,所以是0x8000000000000000

实际的答案采用了溢出的方式,双精度最大的表示大概是1.8*10的308次方,所以可以使用让正无穷等于 1e400之类溢出即可。然后负无穷等于负的正无穷,然后用1/负无穷就得到负0。

类型转换的时候,int之前已经知道,转换成float可以转换,但是会被舍入。而int和float转成double都没有问题,因为double的位数太多,覆盖了int的32位和float的23位,所以不会损失精度。

double 转 float就可能溢出或者损失精度。而从float或者double转成int,由于不带小数,值会向0舍入,也可能会溢出,因为可表示范围要大于int。与intel兼容的处理器,在浮点数转换成int如果找不到对应的值时,会生成最小的int值即-2147483648.

但是在实际实验的时候,发现溢出的double转int的时候, (int)1e100会得到int的正上限即2147483647, (unsigned int) 1e190则会得到-1, 因为是转成了111111…

2.54 类型转换

  1. x == (int)(double)x, int转double 不会损失精度,在转会double,由于在int范围内,所以没有问题
  2. x == (int)(float)x, 从上次做过的题目可以知道,int转float可能会被舍入,最小的正整数是2的24次方+1 = 16,777,2167
  3. d == (double)(float)d, 这个简单, 任何一个大于float上限的double或者阶码转换的之后精度超过24位的double转成float都会损失精度
  4. f == (float)(double)f, 这个是没有问题的.
  5. f == -(-f), 这个也没有问题,浮点数的符号转换就是转换最高位.
  6. 1.0/2 == 1/2.0, 都是先转换然后除,不会有问题
  7. d * d >= 0.0这个也成立
  8. (f + d) - f == d这个不一定.有可能f很大而d很小,f吞掉d,最后得到0.

第二章顺利看完了,记得一年多前刚买这书的时候初看还感觉和看天书一样. 现在第二章也顺利搞定了. 再把后面的家庭作业做做.

下一章是程序的机器级表示也就是汇编, 今天下午整体过了一遍, 感觉用心一点应该还是可以看懂的. 加油吧. 今天王爽的汇编语言也到了, 结合着一起看.