浮点数在内存中的存储结构
基础概念
- 十进制整数转二进制为除 2 取余,逆序排列
- 十进制小数转二进制为乘 2 取整,顺序排列
- 二进制转十进制是每位加权求和
\[ 11.1101_2=1*2^1+1*2^0+1*2^{-1}+1*2^{-2}+0*2^{-3}+1*2^{-4} \]
n 进制的科学计数法 \(a*n^b\) (\(0<|a|<n\)) 其中 a 和 b 都是 n 进制数字,类比十进制,b 表示 n 进制指数 (小数点移动的位数)
E. G. 二进制 \(10000_2=1*2^{100_2}\), 指数相当于十进制的 4, 也就是小数点向右移动 4 位
浮点数国际标准 IEEE 754
国际标准 IEEE 754 标准使用一个三元组 \(\{S, E, M\}\) 表示一个浮点数 N
S
符号位,0 和 1 分别表示正和负E
阶码,用移码表示M
尾码,按照 IEEE 754 标准存储浮点数需要先规格化, 最后的尾码一定是 \((1.xxxxx)_2\) 的二进制格式,第一位一定是 1, 因此尾码存储时舍去了第 1 位的 1, 只保存了二进制小数部分,这样可以多表示一位
浮点数的规格化
同一个浮点数的表示规格并不统一,比如 \((1.11)_2\times 2^0 = (0.111)_2\times 2^1 = (0.0111)_2\times 2^2\).
为了数据的表示精度,就必须充分利用尾码的有效位数,IEEE 754 标准中当尾码大小不是 \((1.xxxx)_2\) 格式时就左右移动小数点并同时修改阶码大小,直到达到要求, 该过程称为浮点数规格化
浮点数的表示
浮点数中的指数 e 可能有正有负,为了方便表示,将 e 增加一个固定的偏移量得到移码 E 作为阶码
平常使用最多的是单精度 (32 位) 和双精度 (64 位) 浮点数, 尾码部分默认小数点前的一位 1 省去了,后面不再赘述
单精度浮点数
符号位 (S) 1 位,阶码 (E) 8 位,尾码 (M) 23 位
其中阶码偏移量为 127 (0x7F), 尾码仅表示小数部分, 前面的 1 和小数点省略
真值表示: \(X=(-1)^S\times (1.M) \times 2^{E - 127}\) \(e=E-127\)
由于 \(2^{23}=8388608\) 最大可表示精度为 7 位,但不能表示所有的 7 位数,可保证 6 位精度
双精度浮点数
符号位 (S) 1 位,阶码 (E) 11 位,尾码 (M) 52 位
其中阶码偏移量为 1023 (0x3FF), 尾码仅表示小数部分, 前面的 1 和小数点省略
真值表示: \(X=(-1)^S\times (1.M) \times 2^{E - 1023}\) \(e=E-1023\)
由于 \(2^{52}=4503599627370496\) 最大表示精度为 16 位,不能表示所有 16 位数字,保证精度为 15 位
特殊值
在特殊情况下,浮点数的计算不再按照常规的方式计算, 而是直接表示特殊的真值,以下对于单精度和双精度都一样
真值 0: 当阶码 E 全 0, 且尾数 M 全 0 时,表示真值 X 为 0, 由于符号位不同, 所以有正负 0 两种表示
无穷大:当阶码 E 全 1, 且尾码 M 全 0 时,表示真值为无穷大, 符号位不同可以表示正负无穷大
NaN: 当阶码全 1, 且尾码 M 非全 0 时,全都表示 NaN, NaN 没有正负之分, 符号位 0 或 1 都是 NaN
编码测试
编写一个函数,用于将数值以二进制打印
1 | template<typename T> |
先看一下 1.0 的结果 print_bin(float(1.0))
结果为
00111111 10000000 00000000 00000000
其中 S=0, \(E=(01111111)_2=127\), M=0
也就是 \(1.0=(-1)^S\times (1.M) \times 2^{E-127}=1\times 1.0 \times 2^0\)
再试试 0.5, print_bin(float(0.5))
结果为
00111111 00000000 00000000 00000000
与 1.0 的区别仅仅是阶码小了 1
真值计算 \(0.5=(-1)^(0)\times (1.0) \times 2^{126-127}=1\times 1.0 \times 2^{-1}\)
再验证一下特殊值是否如我们所想
1 | printf("%f\n", std::numeric_limits<float>::infinity()); |
输出结果如下
1 | inf |
可以发现,对于无穷大,printf 以字符串 inf 表示了,
quiet_NaN()
获取的 NaN 值是阶码全 1, 尾码的第一位设置为 1,
改变符号位依然是 NaN, 我试了一下在无穷大的基础上,
将尾码倒数第 4 位 (随机一位) 设置为 1, 输出为 NaN, 符合预期