本文不是详细教程、是我的笔记,我重新梳理了浮点数相关的知识,所以不会写的特别详细、不会详细介绍 IEEE754 浮点数标准。
本文用 B(binary) 下标表示二进制,用 D (decimal)表示 10 进制。
先说几个基础知识:
- 二进制小数比如 0.125B * 2的1次方 = 1.25B,1.25B * 2的 -1 次方 = 0.125B
也就是说:二进制数字,乘以 2 的 N 次方,则小数点往右边移动 N 位;乘以 2 的 -N 次方,则小数点往左边移动 N 位。 - 十进制小数转换成的二进制小数,和浮点数如何在计算机中存储,是两个问题!!
十进制的整数,在计算机中是怎么用二进制存储的?这个程序员基本都知道,大概过程就是将十进制转换成二进制的 0 和 1 然后存储,这个很容易理解。
不好理解的是,十进制的小数,比如 0.125 以什么样的二进制形式存储在计算机中呢?
再次插入两个基本概念:定点数和浮点数。
“定点”定的是“小数点”!所以定点数也可以表示整数和小数,定点数如果要表示整数或小数,分为以下三种情况:
- 1. 纯整数:例如整数100,小数点其实在最后一位,所以忽略不写
- 2. 纯小数:例如:0.123,小数点固定在最高位
- 3. 整数+小数:例如1.24、10.34,小数点在指定某个位置
对于前两种情况,纯整数和纯小数,因为小数点固定在最低位和最高位,所以它们用定点数表示时,原理是相同的,只需要把整数部分、小数部分,按照十进制转二进制的规则,分别转换即可。
而对于整数 + 小数的情况,用定点表示时,需要约定小数点的位置,才能在计算机中表示。
定点数表示整数+小数
种情况下,我们需要先约定小数点的位置。
依旧以 1 个字节(8 bit)为例,我们可以约定前 5 位表示整数部分,后 3 位表示小数部分。
对于数字 1.5 用定点数表示就是这样:
1 |
1.5(D) = 00001 100(B) |
这就是用定点数表示一个小数的方式。这里再总结一下这个过程:
- 1. 在有限的 bit 宽度下,先约定小数点的位置
- 2. 整数部分和小数部分,分别转换为二进制表示
- 3. 两部分二进制组合起来,即是结果
但是有没有发现一个问题,我们约定了前 5 位表示整数部分,后 3 位表示小数部分,此时这个整数部分的二进制最大值只能是 11111,即十进制的 31,小数部分的二进制最大只能表示 0.111,即十进制的 0.875。
如果我们想要表示更大范围的值,怎么办?
- 1.扩大 bit 的宽度:例如使用 2 个字节、4 个字节,这样整数部分和小数部分宽度增加,表示范围也就变大了。
- 2.改变小数点的位置:小数点向后移动,整个数字范围就会扩大,但是小数部分的精度就会越来越低,没有办法表示类似 0.00001 这种高精度的值。
由此我们发现,不管如何约定小数点的位置,都会存在以下问题:
- 数值的表示范围有限(小数点越靠左,整个数值范围越小)
- 数值的精度范围有限(小数点越靠右,数值精度越低)
总的来说,就是用定点数表示的小数,不仅数值的范围表示有限,而且其精度也很低。要想解决这 2 个问题,所以人们就提出了使用「浮点数」的方式表示数字。
浮点数
何为“浮点”?如下图,同一个数,小数点位置不同,乘以 10 的 N 次幂的 N 值就不同了!
8.345 = 8.345 * 10^0
8.345 = 83.45 * 10^-1
8.345 = 834.5 * 10^-2

如上,float 浮点数,第一位是标志位(表示正负,0表示正,1表示负),之后8位表示“幂(exponent)”,再之后23位表示“尾数(mantissa)”,这个“尾数”是不是感觉有点不好理解,你可以理解为 “83.45 * 10^-1” 中的 83.45 。
举例:将十进制数 25.125 转换为浮点数
整数部分:25(D) = 11001(B)
小数部分:0.125(D) = 0.001(B)
用二进制科学计数法表示:25.125(D) = 11001.001(B) = 1.1001001 * 2^4(B)
OK,到这里,“尾数(mantissa)”已经确定是 1.1001001 了,而“幂(exponent)”是 4 ,在二进制中“底数”默认是 2 。
这里有几个地方要注意
关于“尾数(mantissa)”注意事项
“尾数(mantissa)”按规范,小数点前面只有一位且必须是1(这也注定了“尾数(mantissa)”的值范围: 1 <= M < 2 ),所以可以省略,这一位可以省略。
关于“幂(exponent)”注意事项
上面说了,“幂(exponent)”一共8个比特位,可表示的值范围是 0-255,但是呢,“幂(exponent)”可以是负数啊,但一共8个比特位,难道还有再拿出来一位表示正负?太浪费了吧!
IEEE 规范是这样处理的:一共8个比特位的“幂(exponent)”可表示的范围是 -127 到 128,但是,“幂(exponent)”的实际值,加上一个中间数 127 后的值当作存在计算机中的“幂(exponent)”,比如 1.1001001 * 2^4(B) 中的“幂(exponent)”是 4 但是存的是时候存 127+4=131 ,即:10000011 。
所以 25.125(D) = 11001.001(B) = 1.1001001 * 2^4(B) 在计算机中是这样存储的:
然后我们再看看,25.125(D) 在计算机中对应的二进制是怎么存的:

浮点数为什么有精度损失?
其实浮点数为什么会有精度损失,跟上面介绍的浮点数的储存形式,没有绝对的关系。
十进制小数,无法全部都用二进制小数表示。
要理解这句话,你可以拿起纸和笔计算一下,将 0.1D 转换成二进制小数,你会发现会无限循环下去。这就说明了“十进制小数,无法全部都用二进制小数表示。”。说明了,其实跟 float、double 关系不大,不管用 float 还是 double 都注定了0.1D 无法精确转换成二进制小数。
我再举个例子,这是我自己想的,来帮助理解“十进制小数,无法全部都用二进制小数表示。”这句话,如下:
对于正整数,不管几进制,都可以完全一对一转换,因为不管几进制,增加或减少的单位都是 1 。
但是小数,是“平分”的过程,十进制小数,是以 10 为单位去平分,比如 0.1 是将1分成10份,0.2 是将1分成10份后取其中两份,0.01是将 1 分成 10*10 份。不同进制之间,平分的单位是不一样的,二进制小数,是以 2 为单位平分。这就注定了,不同进制的小数,无法一一对应。
几乎每篇介绍浮点类型 float 的时候都会说:
浮点数 float
的精度通常被认为是 6到7位有效数字。
那么,浮点数 float 的精度通常被认为是6到7位有效数字,“6到7位有效数”具体是什么意思?怎么理解呢?
1. 什么是有效数字?
有效数字是指一个数字中从第一个非零数字开始,到最后一位数字为止的所有数字。例如:
123.456
有 6 位有效数字。0.00123
有 3 位有效数字(前导零不算)。【看过前面对浮点数存储的表述,你应该能明白为什么前导0都不算了,因为尾数的首位默认都是1】1.2300
有 5 位有效数字(末尾的零也算)。
2. 为什么是6到7位有效数字?
- 23 位尾数 可以表示的最大二进制精度是 2的23次方 = 8,388,608223 = 8,388,608,这大约相当于 7 位十进制数字。【结合下面例子理解,我这里简单说下,这里的“精度”指的是“尾数”,float中尾数占23个比特位,可以表示6到7位十进制数字。】
- 但由于浮点数的存储方式和舍入误差,实际精度可能会稍微降低,因此通常认为
float
的精度是 6到7位有效数字。
再结合下面例子理解:
1 2 |
float a = 123.456f; // 6位有效数字 System.out.println(a); // 输出 123.456 |
上面这个例子中,123.456
是 6 位有效数字,float
可以准确表示。
1 2 |
float b = 123.456789f; // 9位有效数字 System.out.println(b); // 输出 123.45679 |
上面这个例子中,123.456789
是 9 位有效数字,float
无法准确表示,最后几位会被舍入,输出结果为 123.45679
。
1 2 3 4 5 |
float c = 1234567f; // 7位有效数字 System.out.println(c); // 输出 1234567.0 float d = 0.000001234567f; // 7位有效数字 System.out.println(d); // 输出 1.234567E-6 |
无论是大数还是小数,float
的精度都限制在 6到7位有效数字。
OK,本文结束,重点是要理解,float 的精度/有效尾数是 6到7 位,指的是“尾数”的精确度!!!