一、浮点数是什么
1.1 从科学计数法说起
十进制里我们这样写很小的数:
0.0034 = 3.4 × 10⁻³
↑ ↑
尾数 指数
浮点数就是二进制的科学计数法:
二进制:1.011 × 2¹⁰
↑ ↑
尾数 指数
计算机存一个浮点数,就是存尾数和指数这两个部分。
1.2 存储结构
一个浮点数在内存中分为四个字段:
┌──────────┬──────────┬──────────┬──────────┐
│ 阶符 │ 阶码 │ 数符 │ 尾数 │
│ (指数正负) │ (指数大小) │ (数值正负) │ (有效数字) │
└──────────┴──────────┴──────────┴──────────┘
| 字段 | 含义 | 举例 |
|---|---|---|
| 阶符 | 指数是正还是负 | + |
| 阶码 | 指数的值 | 3 |
| 数符 | 整个数是正还是负 | + |
| 尾数 | 有效数字部分 | 1.011 |
二、精度与范围:考试核心考点
记住两条规则:
① 尾数越长 → 精度越高(能表示的有效数字越多)
② 阶码越长 → 范围越大(能表示的数越大或越小)
打个比方:
尾数好比尺子的刻度 → 刻度越细,量得越准(精度)
阶码好比尺子的长度 → 尺子越长,量得越远(范围)
典型考题
浮点数 A:阶码 4 位,尾数 8 位(共 12 位) 浮点数 B:阶码 6 位,尾数 6 位(共 12 位)
总位数相同,分配不同:
| 阶码 | 尾数 | 特点 | |
|---|---|---|---|
| A | 4 位(短) | 8 位(长) | 范围小,精度高 |
| B | 6 位(长) | 6 位(短) | 范围大,精度低 |
阶码和尾数此消彼长,总位数固定的条件下,精度和范围不可兼得。
三、规格化
3.1 什么是规格化
规则:尾数的小数点后第一位必须是 1,不能是 0。
规格化: 1.0110 × 2³ ✓ 小数点后第一位是 1
未规格化:0.0110 × 2⁴ ✗ 小数点后第一位是 0
3.2 如何规格化
未规格化的数可以调整指数来修正:
0.0110 × 2⁴ = 0.110 × 2³ = 1.10 × 2²
↑
已规格化
小数点右移一位,指数就减 1,值不变。
3.3 为什么要规格化
如果不规格化,0.0110 的前两位 00 是浪费的——占了尾数位却没有贡献有效数字。规格化确保每一位尾数都承载有效信息,不浪费精度。
四、IEEE 754 标准——浮点数的真实存储格式
你在 Java 里用的 float 和 double,底层遵循的就是 IEEE 754 标准。
4.1 两种精度
float(单精度):32 位
┌──────┬──────────┬───────────────────────────┐
│ 符号 │ 指数 │ 尾数 │
│ 1 位 │ 8 位 │ 23 位 │
└──────┴──────────┴───────────────────────────┘
double(双精度):64 位
┌──────┬──────────┬───────────────────────────┐
│ 符号 │ 指数 │ 尾数 │
│ 1 位 │ 11 位 │ 52 位 │
└──────┴──────────┴───────────────────────────┘
| float | double | |
|---|---|---|
| 总位数 | 32 | 64 |
| 符号位 | 1 位 | 1 位 |
| 指数位 | 8 位 | 11 位 |
| 尾数位 | 23 位 | 52 位 |
| 精度 | 约 7 位有效数字 | 约 15~16 位有效数字 |
| Java 声明 | float f = 1.0f; | double d = 1.0;(默认) |
尾数越多,精度越高。 double 的尾数是 float 的两倍多,精度自然高得多。
4.2 指数的偏移量
IEEE 754 中,指数不用补码表示,而是用偏移量(Bias):
| 类型 | 指数位数 | 偏移量 |
|---|---|---|
| float | 8 位 | 127 |
| double | 11 位 | 1023 |
存储值 = 实际指数 + 偏移量
例如 float 中,实际指数为 3:
存储值 = 3 + 127 = 130 = 10000010
读取时减去偏移量即可还原。
五、完整手算示例:float 存储 9.625
第一步:整数和小数分别转二进制
整数部分:9 ÷ 2 ... 余 1
÷ 2 ... 余 0
÷ 2 ... 余 0
÷ 2 ... 余 1
9 = 1001(二进制)
小数部分:0.625 × 2 = 1.25 → 取 1
0.25 × 2 = 0.5 → 取 0
0.5 × 2 = 1.0 → 取 1
0.625 = .101(二进制)
合起来:9.625 = 1001.101(二进制)
第二步:规格化
1001.101 = 1.001101 × 2³
↑ ↑
尾数 指数 = 3(小数点左移了 3 位)
第三步:填入 32 位
符号:0(正数)
指数:3 + 127 = 130 = 10000010
尾数:00110100000000000000000(去掉开头的 1,后面补 0 凑满 23 位)
完整 32 位:
0 10000010 00110100000000000000000
↑ ↑ ↑
符号 指数 尾数
六、为什么 0.1 + 0.2 ≠ 0.3
这是浮点数最经典的面试题和实际问题。
6.1 根本原因:有些十进制小数在二进制中无限循环
十进制中 1/3 = 0.333333... 无限循环,用有限位数永远写不精确。
二进制也有同样的问题。把十进制 0.1 转成二进制:
0.1 × 2 = 0.2 → 取 0
0.2 × 2 = 0.4 → 取 0
0.4 × 2 = 0.8 → 取 0
0.8 × 2 = 1.6 → 取 1
0.6 × 2 = 1.2 → 取 1
0.2 × 2 = 0.4 → 取 0 ← 回到起点,开始循环
...
十进制 0.1 = 二进制 0.00011001100110011...(无限循环)
double 只有 52 位尾数,存不下无限循环,必须截断。截断就有误差,两个带误差的数相加,误差累积,结果就不是精确的 0.3。
这不是编程语言的 bug,是浮点数的先天局限。
6.2 解决方案
需要精确计算时(比如金额),Java 中使用 BigDecimal:
BigDecimal a = new BigDecimal("0.1");
BigDecimal b = new BigDecimal("0.2");
System.out.println(a.add(b)); // 输出 0.3,精确
七、为什么指数用偏移量而不用补码
7.1 补码存指数的问题
用 4 位举例,假设两个浮点数要比较大小:
A 的指数:1101(补码,实际 -3)
B 的指数:0010(补码,实际 +2)
直接按二进制比较:1101 > 0010,但实际 -3 < +2。
结果相反。 计算机必须先识别符号位,再分别处理,才能正确比较。电路复杂,速度慢。
7.2 偏移量存指数的优势
偏移量 = 7 时:
A 的指数:-3 + 7 = 4 → 0100
B 的指数:+2 + 7 = 9 → 1001
直接比较:0100 < 1001,即 -3 < +2,正确。
偏移量把整个范围平移到了正数区间,存储值的大小顺序就是实际值的大小顺序:【核心】
实际指数: -7 -6 -5 -4 -3 -2 -1 0 1 2 3 4 5 6 7 8
存储值: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
这意味着计算机比较两个浮点数时,可以把符号位、指数、尾数拼在一起,当作一个普通整数直接比较,不需要任何特殊处理。这在硬件层面省了大量的电路和时间。
偏移量不是为了表示正负(补码也能),而是为了让比较操作变得简单快速。
八、速查卡片
┌──────────────────────────────────────────────────┐
│ 浮点数 = 符号 + 指数 + 尾数 │
│ │
│ 尾数越长 → 精度越高 │
│ 指数越长 → 范围越大 │
│ 精度和范围此消彼长 │
│ │
│ 规格化:尾数小数点后第一位必须是 1 │
│ │
│ IEEE 754 标准: │
│ float = 32 位(1 + 8 + 23),指数偏移 127 │
│ double = 64 位(1 + 11 + 52),指数偏移 1023 │
│ │
│ 存储值 = 实际指数 + 偏移量 │
│ 偏移量的作用:让位模式的大小顺序=数值大小顺序 │
│ │
│ 0.1 + 0.2 ≠ 0.3 是浮点数先天局限,不是 bug │
│ 需要精确计算 → 用 BigDecimal │
└──────────────────────────────────────────────────┘