小数怎么表示 -- 浮点数与 IEEE 754
本讲你将理解:小数如何在只有 0 和 1 的世界里表示,以及为什么浮点数会有精度误差。
生活化类比:科学计数法
你在物理课上一定见过科学计数法:
光速 = 3.0 x 108 m/s,电子质量 = 9.1 x 10-31 kg。
这种方式用很少的数字就能表示非常大或非常小的数。
计算机表示小数用的也是完全相同的思路——二进制科学计数法。
任何二进制小数都可以写成:
1.xxxxxxxxxx x 2yyyyy
这个形式只需要存储三样东西:
- 符号:正号还是负号?
- 尾数(xxxxxxxxxx):有效数字部分
- 指数(yyyyy):小数点的位置
IEEE 754 标准就是把这三样东西按固定格式塞进 32 个 bit 里。
IEEE 754 单精度的三段结构
32 位(单精度 float)被精确地划分为三段:
第一段:符号位(1 bit)
最简单的一段。
0 表示正数,1 表示负数。
第二段:指数位(8 bits)
指数决定小数点的位置(这就是「浮点」名字的由来——小数点可以「浮动」)。
但指数不是直接存的,而是存储「实际指数 + 127」。
这个 127 叫做偏移量(Bias)。
为什么要加偏移量?
因为 8 位只能表示 0~255(无符号),但实际指数需要是负数(表示小于 1 的数)。
加偏移量 127 后:
- 存储值 127 对应实际指数 0(127 - 127 = 0)
- 存储值 0 对应实际指数 -127
- 存储值 255 对应实际指数 +128
第三段:尾数位(23 bits)
尾数存储的是「1.xxxxx」中的「xxxxx」部分,省略了前导的 1。
因为二进制科学计数法中,归一化后的形式总是 1.xxx,所以前面的 1 不需要存储。
这巧妙地多节省了 1 bit 的精度。
理解三段结构的关键:浮点数 = (-1)^符号 x 1.尾数 x 2(指数-127)。这就是 IEEE 754 的全部精髓。
交互演示:32 位浮点数拆解器
输入一个浮点数,观察它在 IEEE 754 单精度格式下的三段结构。
分段横条会高亮显示三段分别对应哪些 bit。
IEEE 754 单精度拆解器
试试输入 0.1,观察尾数部分。
0.1 的二进制尾数是一个无限循环小数——23 位完全不够存储。
这就引出了一个著名的计算机现象。
为什么 0.1 + 0.2 不等于 0.3
如果你在 Python 或 JavaScript 中运行 0.1 + 0.2 == 0.3,结果是 False。
这不是 bug,而是浮点数表示法的必然结果。
原因很简单:0.1 无法用二进制精确表示。
就像十进制中,1/3 = 0.33333... 是一个无限循环小数。
二进制中,0.1 (十进制) = 0.000110011001100110011... (二进制),也是一个无限循环小数。
IEEE 754 只有 23 位尾数来存储这个无限循环小数。
超出 23 位的部分只能被截断(舍入),这就产生了舍入误差。
0.1 在 float32 中实际存储的值大约是 0.100000001490116119384765625。
0.2 在 float32 中实际存储的值大约是 0.20000000298023223876953125。
两者相加再截断后,结果不等于精确的 0.3。
金额计算必须避免浮点数
在涉及金额的场景,应该使用整数(以「分」为单位)或专门的 Decimal 类型。
Python 的 Decimal、Java 的 BigDecimal、C# 的 decimal 都是为此设计的。
用浮点数存储金额,等你发现 0.01 的累积误差变成 0.010000000000000002 时,问题已经种下很久了。
浮点数的取值范围与特殊值
IEEE 754 为指数和尾数保留了几个特殊组合:
| 类别 | 符号位 | 指数 (存储) | 尾数 | 含义 |
|---|---|---|---|---|
| 零 | 0 或 1 | 0 (全 0) | 全 0 | +0 或 -0 |
| 非归一化数 | 0 或 1 | 0 (全 0) | 非全 0 | 非常接近 0 的值 |
| 归一化数 | 0 或 1 | 1 ~ 254 | 任意 | 正常的浮点数 |
| 无穷大 | 0 或 1 | 255 (全 1) | 全 0 | +Infinity 或 -Infinity |
| NaN | 任意 | 255 (全 1) | 非全 0 | 非数值(如 0/0) |
单精度 float (32 位) 的近似范围:
- 最大正值:约 3.4 x 1038
- 最小正值(归一化):约 1.2 x 10-38
- 最小正值(非归一化):约 1.4 x 10-45
- 有效精度:约 7 位十进制有效数字
双精度 double (64 位) 的范围更大、精度更高:
- 11 位指数(偏移量 1023)
- 52 位尾数
- 有效精度:约 15~16 位十进制有效数字
浮点数精度损失的三种情况
情况一:十进制到二进制的转换误差
如前面 0.1 的例子。
任何在十进制中是「干净」的小数,在二进制中都可能是无限循环的。
只有分母是 2 的幂的分数(如 0.5、0.25、0.125)才能在二进制中精确表示。
情况二:大数加小数
浮点数的精度不是均匀分布的——数值越大,能区分的最小差值越大。
比如 16777216.0 + 1.0 = 16777216.0(在 float32 中结果不变)。
因为 32 位浮点数只能精确表示约 7 位有效数字,16777216 本身已经占了 8 位。
加 1 产生的变化落入了尾数无法表示的精度范围,直接被「吞掉」了。
情况三:相近数相减
两个非常接近的浮点数相减,会导致有效数字的灾难性丢失。
比如 1.2345678 - 1.2345677 = 0.0000001,但实际计算可能得到 0.000000099999... 这样误差很大的结果。
了解浮点数的精度限制不是让你「害怕」用它,而是让你在合适的场景选择合适的工具。科学计算、图形渲染、机器学习——浮点数在这些领域完全够用。但金融、会计、精确计数——请远离浮点数。
代码演示:浮点数的秘密
实例
import struct
def float_to_ieee754_binary(num):
"""将浮点数转换为 IEEE 754 单精度三段表示"""
# 使用 struct 将浮点数打包为 4 字节(大端序)
packed = struct.pack('>f', num)
# 转换为 32 位二进制字符串
bits = ''.join(f'{b:08b}' for b in packed)
sign = bits[0]
exponent = bits[1:9]
mantissa = bits[9:32]
exp_stored = int(exponent, 2)
exp_actual = exp_stored - 127
# 计算尾数的小数值
frac = 0
for i, bit in enumerate(mantissa):
if bit == '1':
frac += 2 ** -(i + 1)
value = (-1 if sign == '1' else 1) * (1 + frac) * (2 ** exp_actual)
return {
'bits': bits,
'sign': sign,
'exponent': exponent,
'mantissa': mantissa,
'exp_stored': exp_stored,
'exp_actual': exp_actual,
'fraction': frac,
'reconstructed': value
}
# =============================================
# 演示 1: 对比 0.5 和 0.1 的表示
# =============================================
print("=" * 60)
print("演示 1: 为什么 0.5 精确而 0.1 不精确")
print("=" * 60)
for num in [0.5, 0.1]:
r = float_to_ieee754_binary(num)
print(f"\n浮点数: {num}")
print(f" 符号位: {r['sign']} ({'正' if r['sign'] == '0' else '负'})")
print(f" 指数位: {r['exponent']} (存储={r['exp_stored']}, 实际={r['exp_actual']})")
print(f" 尾数位: {r['mantissa']}")
print(f" 实际存储值: {r['reconstructed']}")
# =============================================
# 演示 2: 经典的 0.1 + 0.2 != 0.3
# =============================================
print("\n" + "=" * 60)
print("演示 2: 0.1 + 0.2 != 0.3")
print("=" * 60)
# 使用更高精度显示
import decimal
ctx = decimal.getcontext()
ctx.prec = 50
a = decimal.Decimal(0.1)
b = decimal.Decimal(0.2)
c = decimal.Decimal(0.3)
print(f"0.1 的实际值: {a}")
print(f"0.2 的实际值: {b}")
print(f"0.3 的实际值: {c}")
print(f"0.1 + 0.2 = {a + b}")
print(f"0.1 + 0.2 == 0.3 ? {0.1 + 0.2 == 0.3}")
# =============================================
# 演示 3: 大数「吞」小数
# =============================================
print("\n" + "=" * 60)
print("演示 3: 大数加小数被吞没")
print("=" * 60)
big = 16777216.0
small = 1.0
result = big + small
print(f"{big} + {small} = {result}")
print(f"结果等于原数? {result == big}")
# 展示这个数达到 float32 的精度极限
print(f"\n原因: {big} 在 float32 中需要 {len(format(struct.unpack('>I', struct.pack('>f', big))[0], '032b')[:23])} 位尾数")
print(f"相邻两个 float32 值的最小间隔约为 2^24 / 2^23 = 2.0")
# =============================================
# 演示 4: 正确的大小写转换技巧
# =============================================
print("\n" + "=" * 60)
print("演示 4: RUNOOB 案例 - 金额的正确处理")
print("=" * 60)
from decimal import Decimal
# 错误做法:用浮点数
float_total = 0.1 + 0.2
print(f"错误(浮点数): 0.1 + 0.2 = {float_total}")
# 正确做法:用 Decimal
decimal_total = Decimal('0.1') + Decimal('0.2')
print(f"正确(Decimal): Decimal('0.1') + Decimal('0.2') = {decimal_total}")
# 或者用整数(以分为单位)
cents_total = 10 + 20 # 0.1元 = 10分, 0.2元 = 20分
print(f"正确(整数分): 10分 + 20分 = {cents_total}分 = {cents_total/100}元")
# =============================================
# 演示 5: 浮点数的特殊值
# =============================================
print("\n" + "=" * 60)
print("演示 5: 浮点数的特殊值")
print("=" * 60)
import math
# 无穷大
print(f"float('inf') = {float('inf')}")
print(f"1.0 / 0.0 = {1.0 / 0.0}")
print(f"float('inf') > 1e1000000 = {float('inf') > 1e1000000}")
# NaN
print(f"float('nan') = {float('nan')}")
print(f"0.0 / 0.0 = {0.0 / 0.0}")
print(f"float('nan') == float('nan') ? {float('nan') == float('nan')}")
print(f" (NaN 不等于任何值,包括它自己!)")
# 检查 NaN
print(f"math.isnan(float('nan')) = {math.isnan(float('nan'))}")
实战建议:何时用什么
| 场景 | 推荐类型 | 原因 |
|---|---|---|
| 科学计算、图形渲染 | float 或 double | 精度足够,硬件加速支持好 |
| 金融金额计算 | Decimal / BigDecimal | 精确十进制运算,无舍入误差 |
| 计数器、索引 | int / long | 精确整数,没有精度问题 |
| 比较浮点数 | 用容差比较 | 不应直接 ==,应用 abs(a-b) < epsilon |
| 货币存储/传输 | 整数(以分为单位) | 避免序列化时的浮点误差 |
