浮点数在计算机中的表示
现实世界中不仅有整数,还有小数,有没有一种编码,能同时表示它们呢。IEEE 754 是 20 世纪 80 年代以来最广泛使用的浮点数运算标准,为许多 CPU 与浮点运算器所采用,简单来说,它采用了类似科学记数法的形式来表示整数和小数。该标准的设计者,William Morton Kahan 教授在 1989 年获得图灵奖。
产生背景
因为 定点数 存在两个缺陷
- 不能有效的表示非常大的数字。
- 例如,表达式 5×2100 是 (101)2 后面跟着 100 个零的位模式来表示,如果用定点表示法,容易产生溢出。
- 不能有效的表示小数。
为了解决上述问题,浮点数应运而生。
表示方法
科学记数法
参照科学记数法,IEEE 浮点标准采用如下形式来表示一个数(Value)
V=(−1)s×M×2E
- (−1)s 表示符号位,当 s=0, V 为正数;当 s=1,V 为负数。
- M 表示有效数字,范围属于 [1,2)。
- 2E 表示指数位。
例 1
十进制的 5.0 写成二进制是 101.0,相当于 (1.01)×22。按照上面的格式,可以得出 s=0,M=1.01,E=2。
有效数字 M
因为 1≤M<2,也就是说,M 的整数部分总是为 1,所以我们可以只保留它的小数(fraction)部分,而将开头的 1 省略。这样一来,就能多表示一位有效数字。
- 在单精度浮点格式中,s、exp、fraction 字段分别为 1、8、23 位。
- 在双精度浮点格式中,s、exp、fraction 字段分别为 1、11、52 位。

指数 E
指数 E 的取值情况比较复杂,我们以 float 浮点类型为例。
- 当 exp 既不全为 0,也不全为 1 时
- E = exp - 127。这里的;127;是一个偏置值;=;27−1。 有效数字;M=1+fraction。
- 当 exp 全为 0 时
- E = 1 - 127。有效数字;M=fraction,;不用在前面加上;1。特别的,当 fraction 全为 0 时,表示 ±0 (取决于符号位)。
- 当 exp 全为 1 时
- 如果 fraction 全为 0,表示 ±∞ (取决于符号位)。
- 如果 fraction 不全为 0,表示 NaN (Not a Number)。
精度问题
十进制和二进制相互转化
先看看十进制数和二进制数如何相互转换。用下标表示数的基,d10 表示十进制数,d2 表示二进制数。
一个具有 n+1 位整数,m 位小数的十进制数 d10 表示为

例 2
(1234.5678)10=1×103+2×102+3×101+4×100+5×10−1+6×10−2+7×10−3+8×10−4
同理,一个具有 n+1 位整数 m 位小数的二进制数 b2 表示为

例 3
(1010.1001)2=1×23+0×22+1×21+0×20+1×2−1+0×2−2+0×2−3+1×2−4
将二进制数转换成十进制数,比较容易,如例 3 所示。而将十进制数转换成二进制数,需要把整数部分和小数部分分开转换,整数部分用 2 除,取余数;小数部分用 2 乘,取整数位。
例 3
把 (13.125)10 转换成二进制数。
整数部分: (13)10=(1101)2
(1);13÷2=6;余数为;1⇒1
(2);6÷2=3;余数为;0⇒01
(3);3÷2=1;余数为;1⇒101
(4);最后得到的商小于;2,;所以不用再继续除了。⇒1101
小数部分: (0.125)10=(001)2
(1);0.125×2=0.25;整数位是;0⇒.0
(2);0.25×2=0.5;整数位是;0⇒.00
(3);0.5×2=1.0;整数位是;1⇒.001
(4);最后得到的乘积是个整数,所以不用再继续乘了。
所以 (13.125)10=(1101.001)2。
一个十进制数能否用二进制浮点数精确表示,关键在于小数部分。 我们来看一个最简单的小数 (0.1)10 能否精确表示。
(1);0.1×2=0.2;整数位是;0⇒.0
(2);0.2×2=0.4;整数位是;0⇒.00
(3);0.4×2=0.8;整数位是;0⇒.000
(4);0.8×2=1.6;整数位是;1⇒.0001
(5);0.6×2=1.2;整数位是;1⇒.00011
(6);0.2×2=0.4;整数位是;0⇒.000110 由第(2)步开始循环。
…
我们得到一个无限循环的二进制小数 (0.1)10=(0.0;0011;0011;0011;…)2。
用有限位无法表示无限循环小数,因此,(0.1)10 无法用 IEEE 754 浮点数精确表示。同时我们还可以得出
(0.2)10=(0.0011;0011…)2
(0.4)10=(0.0110;0110…)2
(0.6)10=(0.1001;1001…)2
(0.8)10=(0.1100;1100…)2
这四个数也无法精确表示。同理
(0.3)10×2=0.6
(0.7)10×2=1.4
(0.9)10×2=1.8
这三个数也无法精确表示。 所以,从 0.1 到 0.9 这 9 个小数中,只有 0.5 可以精确表示
(0.5)10=(0.1)2
二进制小数能精确表示的十进制小数的基本规律
一个十进制小数要能用浮点数精确表示,最后一位必须是 5,因为 1 除以 2 永远是 0.5。当然,这是必要条件,并非充分条件。

一个 m 位二进制小数能够精确表示的小数有多少个呢?
当然是 2m 个。而 m 位十进制小数有 10m个,因此,能精确表示的十进制小数的比例是 2m10m=(0.2)m。m 越大,比例值越小。
其实不管是用十进制、二进制或者其它进制,都存在着有限位数无法精确表示的数字。这时候就需要进行舍入。
舍入
IEEE 标准列出 4 种不同的舍入方法
- 最近舍入
- 舍入到最接近,在一样接近的情况下偶数优先(Ties To Even,这是默认的舍入方式)。
- 朝 +∞ 方向舍入
- 朝 −∞ 方向舍入
- 朝 0 方向舍入
代码验证
show-byte.c
1 | #include<stdio.h> |

little-big-endian
1 | #include <stdio.h> |

我的电脑是 小端序,最低位先输出。
(1.1)10
(3f8ccccd)16=(0011;1111;1000;1100;1100;1100;1100;1101)2
- 符号位 S = 0
- 指数 E = exp-127 = 127-127 = 0
- 小数部分 = 000;¯1100
- 有效数字 M = 1.000 1100 1100 1100 1100 1101
- 舍入 11⇒1
- Value=(−1)0×(1.000;1100;…;1101)×20=1.000;1100;1100;1100;1100;1101
(3ff99999999999a)16=(0011;1111;1111;0001;1001;1001;…;1001;1010)2
- 符号位 S = 0
- 指数位 E = exp-1023 = 1023-1023 = 0
- 小数部分 = 0001;¯1001
- 有效数字 M = 1.0001 1001 1001 1001 … 1010
- 舍入 10⇒1
- Value=(−1)0×(1.0001;1001;…;1010)×20=1.0001;1001;1001;…;1001;1010
(1.3)10
(3fa66666)16=(0011;1111;1010;0110;0110;0110;0110;0110)2
- 小数部分 = 010;¯0110
- 舍入 01⇒0
(3ff4cccccccccccd)16=(0011;1111;1111;0100;1100;1100;…;1100;1101)2
- 小数部分 = 0100;¯1100
- 舍入 11⇒1
(1.7)10
(3fd9999a)16=(0011;1111;1101;1001;1001;1001;1001;1001)2
- 小数部分 = 101;¯1001
- 舍入 10⇒1
(3ffb333333333333)16=(0011;1111;1111;1011;0011;0011;…;0011;0011)2
- 小数部分 = (1011;¯0011)
- 舍入 00⇒0
(1.9)10
(3ff33333)16=(0011;1111;1111;0011;0011;0011;0011;0011)2
- 小数部分 = 111;¯0011
- 舍入 00⇒0
(3ffe666666666666)16=(0011;1111;1111;1110;0110;0110;…;0110;0110)2
- 小数部分 = (1110;¯0110)
- 舍入 01⇒0
拓展
Q1
为什么 exp 需要加上一个偏置值?
A1
采用这种方式表示的目的是简化比较。因为,指数的值可能为正也可能为负,如果采用补码表示的话,符号位 S 和 Exp 自身的符号位将导致不能简单的进行大小比较。正因为如此,指数部分通常采用一个无符号的正数值存储。