浮点数:IEEE 754 掰开揉碎

总篇 00-信息的表示与处理 第五章是浮点数的速查表,这篇负责把每个数字掰开:0.1 为什么存不精确(完整推导)、Bias 为什么是 127、float 的精度到底怎么算的、408 要考的对阶是怎么回事。

一、从科学计数法说起

IEEE 754 的思想完全就是十进制科学计数法的二进制版:

十进制:1234.5 = 1.2345 × 10³
二进制:1100.011 = 1.100011 × 2³

统一形式:V = (-1)^s × M × 2^E

s     符号位(Sign)—— 决定正负
E     指数(Exponent)—— 决定数量级(小数点挪几位)
M     尾数(Mantissa)—— 决定有效数字(精度)

"规格化"就是小数点前留一位非零数字。二进制里更妙:非零只能是 1,所以这一位根本不用存——这就是"隐含的前导 1",凭空多出一位精度。

二、三个字段怎么分工

单精度 float32 位):
┌──────┬──────────┬───────────────────────┐
│ 1位  │   8位    │         23位          │
│  s   │   exp    │         frac          │
└──────┴──────────┴───────────────────────┘

双精度 double64 位):
┌──────┬───────────┬────────────────────────────────────┐
│ 1位  │   11位    │              52位                  │
└──────┴───────────┴────────────────────────────────────┘
字段 float double 作用
s 1 位 1 位 符号
exp 8 位 11 位 存"移码后的指数"
frac 23 位 52 位 存小数点后的尾数

🤔 为什么指数不直接存,要减个 Bias(float 是 127)?

exp 是个 8 位无符号数(0~255),实际指数 E = exp - 127,这样 E 的范围是 -126 ~ 127——正负指数都覆盖了,却不用补码。好处有两个:

  1. 比较浮点数大小时,exp 按无符号比就行,硬件简单
  2. 更精妙的是:同符号浮点数的位模式字典序 = 数值大小序,可以把浮点数当整数排序

(用补码存指数的话,负指数的最高位是 1,按位比较就乱套了。)

三、完整例题:8.25 的编码

整数会转二进制(01-进制与进制转换),小数部分用乘 2 取整

8.25 = 1000.01₂            (0.25 = 0.01,恰好精确)

规格化:1.00001 × 2³
  s = 0
  E = 3exp = 3 + 127 = 130 = 1000 0010
  frac = 00001 后面补零到 23 位

拼起来:
0 | 10000010 | 00001000000000000000000
= 0100 0001 0000 0100 0000 0000 0000 0000
= 0x41040000

8.25 是运气好的——小数部分 0.25 = 2⁻²,精确。运气不好的看下面。

四、0.1 为什么存不精确:完整推导

0.1 的二进制,小数部分用乘 2 取整法一路乘下去:

0.1 × 2 = 0.20
0.2 × 2 = 0.40
0.4 × 2 = 0.80
0.8 × 2 = 1.61
0.6 × 2 = 1.21
0.2 × 2 = 0.40    ← 从这开始循环
...

0.1 = 0.000110011001100110011...₂    (0011 无限循环)
    = 1.1001100110011... × 2⁻⁴

循环了。 就像十进制写 1/3 = 0.3333… 写不完一样,0.1 在二进制里是无限循环小数,而 frac 只有 23 位(double 52 位),必须截断+舍入:

float(0.1):
  E = -4 → exp = 123 = 0111 1011
  frac 取 1.10011001100110011001100|1100...
                                 ↑ 第 24 位开始还是循环的 1100
  向偶数舍入进位 →

  0 | 01111011 | 10011001100110011001101
  = 0x3DCCCCCD

所以内存里的 0.1 其实是 0.10000000149...(float)——不是 0.1,是个很接近它的数。0.2 同理。两个"近似 0.1"相加,误差互相记账:

0.1 + 0.2 = 0.30000000000000004   (double)
0.1 × 3  = 0.30000000000000004
0.110 次 = 0.99999999999999991.0

💡 这和总篇"连续与离散"一节是同一件事的两面:浮点数是离散的、稠密分布在数轴上,0.1 恰好不落在任何格点上。 换 double 只是把格点变密,不改变"不精确"的本质。

五、精度账:float 到底能精确到哪

有效数字位数:

类型 有效位 十进制有效数字 来源
float 24 位(23 frac + 隐含 1) ≈ 7 位 24 × log₁₀2 ≈ 7.2
double 53 位(52 + 1) ≈ 15~16 位 53 × log₁₀2 ≈ 16

"约 7 位"的具体含义——2²⁴ = 16777216 是 float 的分水岭

float f = 16777216.0;  f + 1 = ?   → 还是 16777216float f = 16777218.0;              → 存成 16777218(偶数才行)
float f = 16777217.0;              → 存成 16777216(舍入到最近的偶数)

因为 2²⁴ 之后相邻两个可表示 float 的间隔已经是 2,2²⁵ 之后是 4……指数每升 1,格点间隔翻一倍——这就是"越远离 0 越稀疏"的原理(总篇 5.4 那句话的账本)。

六、三种情况与那两个 0

exp 含义 E 与 M
既不全 0 也不全 1 规格化 E = exp - Bias,M = 1.frac(隐含 1)
全 0 非规格化 E = 1 - Bias,M = 0.frac(无隐含 1)
全 1 特殊值 frac=0 → ±∞;frac≠0 → NaN

非规格化不只是"多存几个小数"——它的妙处在平滑衔接:最小非规格化数与最小规格化数之间的间隔,和规格化区间内部的间隔一样大(都是 2⁻¹⁴⁹),下溢时精度均匀过渡,不会突然掉崖。

0 有两个:+0(全 0)和 -0(符号位 1 其余全 0)。比较时 -0.0 == 0.0(IEEE 规定),但 1.0 / -0.0 = -∞1.0 / +0.0 = +∞——除法才暴露它们的身份。

NaN 的自白:√(-1)0.0/0.0∞-∞ 都产 NaN,而且 NaN 和任何数比较都是 false,包括和自己(NaN == NaN 为 false)——判断 NaN 得用"自己不等于自己"。

七、舍入:向偶数舍入为什么公平

IEEE 754 默认向偶数舍入:恰好卡在中间时,舍入到末位是偶数的那个。

1.52      2.52      3.54

为什么不用"四舍五入"?因为四舍五入逢 .5 必进位,大量数据累加会单方向漂移;向偶数舍入时 .5 有一半进、一半舍,统计上抵消。这也是浮点版"银行家舍入"的由来。

八、408 考点:对阶

浮点加法的流程:对阶 → 尾数相加 → 规格化 → 舍入 → 溢出判断

对阶的核心规则:小阶向大阶看齐

为什么不能大阶向小阶对?——对阶靠移尾数:阶小的尾数要右移(丢低位,损失小数位精度);如果反过来让阶大的尾数左移,丢掉的是高位有效数字,直接毁掉这个数。两害相权取其轻。

例:1.00×2⁴ + 1.80×2² → 把后者变成 0.018×2⁴(尾数右移 2 位)再相加。

九、实战避坑清单

场景 错误姿势 正确姿势
金额计算 float/double 存钱 整数分做单位;Java 用 BigDecimal,MySQL 用 DECIMAL,Python 用 Decimal
浮点比较 a == b fabs(a - b) < EPSILON
累加求和 直接 sum += x 一路加到底 数据量大时先分组/排序再累加(提一句:Kahan 补偿求和专门治这个,算法区再会)
判 NaN x == NaN(永远 false) x != x 或语言内置 isNaN
除零期待异常 浮点除零不抛异常 返回 ±∞ / NaN,要自己检查 isinf/isnan

⚠️ 2026 年还有一个经典事故模式要记住:JSON 里的金额到了 JS 的 Number(就是 double)手里会自己长毛——后端发 0.1,前端显示 0.1000000000000000055。金额请用字符串或整数分传输。

十、盲点自测

  1. 为什么 frac 前面的 1 不用存? ——规格化后小数点前必为 1,二进制非 0 即 1,省一位硬件,凭空多一位精度。
  2. Bias 为什么是 127 而不是 128? ——规格化 exp 取 1~254,E = exp-127 得 -126~127,正负范围最对称;存 128 会让正指数少一档。且无符号 exp 让"位模式序=数值序"这个金性质成立。
  3. 0.1 + 0.2 ≠ 0.3 的病根? ——0.1 和 0.2 在二进制下都是无限循环小数,存储时被舍入成近似值,加法后误差显形。换 double 只是误差变小,不会消失。
  4. float 能精确表示所有 int 吗? ——不能。23+1 位有效 vs int 的 31 位数值位,2²⁴ 之后 float 开始跳数。
  5. 对阶为什么小阶向大阶? ——尾数右移只丢低位、左移丢高位;丢高位等于毁数。
  6. -0.0 和 0.0 相等吗? ——== 为真,但 1/x 结果一正一负无穷,除法才能分辨。
  7. 怎么判断一个数是 NaN? ——x != x(NaN 是唯一不等于自己的值),或用内置 isNaN。
  8. 浮点除零会崩吗? ——不会,返回 ±∞ 或 NaN;整数除零才会崩。

十一、动手玩一把

#include <stdio.h>
#include <math.h>
int main() {
    float f = 0.1f;
    double d = 0.1;
    printf("float  0.1 = %a\n", f);   // 0x1.99999ap-4  ← 就是那个近似值
    printf("double 0.1 = %a\n", d);   // 0x1.999999999999ap-4
    printf("0.1+0.2 == 0.3 ? %d\n", 0.1 + 0.2 == 0.3);   // 0
    printf("%.20f\n", 0.1 + 0.2);                         // 0.30000000000000004000
    float big = 16777216.0f;
    printf("16777216+1 = %.1f\n", big + 1);               // 16777216.0,加不动了
    printf("NaN == NaN ? %d\n", sqrt(-1) == sqrt(-1));    // 0
    return 0;
}

%a 是十六进制浮点格式,一眼看穿机器里存的是什么。


数的表示到这就彻底闭环了:整数有补码、小数有 IEEE 754。还差一块拼图——文字怎么变成 01?→ 05-字符编码

⬅️ 位运算应用 🏠 00-信息的表示与处理 ➡️ 字符编码