浮点数和 IEEE 754
浮点数和 IEEE 754
浮点数的表示格式
为什么需要浮点数?
定点数的小数点位置固定,表示范围非常有限。比如 32 位定点数,若 16 位整数 + 16 位小数,最大只能表示约 65536。
浮点数的思路:模仿科学计数法,让小数点可以”浮动”,用有限的位数表示极大的动态范围。
三个核心字段
| 字段 | 含义 | 作用 |
|---|---|---|
| 符号 s(sign) | 0 表示正,1 表示负 | 决定正负 |
| 尾数 M(mantissa / significand) | 有效数字部分 | 决定精度 |
| 阶码 E(exponent) | 指数,用 移码 存储 | 决定范围 |
阶码使用移码而非补码的原因,见 数据表示#移码。核心原因:移码让硬件用无符号比较器就能正确比较浮点数大小。
真值公式
通用形式:
其中 是基数(二进制下 )。尾数 通常是纯小数或带一位整数。
浮点数的规格化
什么是规格化?
当一个浮点数有多种等价表示时,硬件处理会很麻烦。例如十进制下:
规格化就是规定统一形式:让尾数的最高位是有效数字。
对于二进制,规格化要求尾数的最高位为 1(即 ):
隐藏位(Hidden Bit)
既然规格化后尾数的整数部分永远是 1,那就不需要存储它。IEEE 754 用这个”隐藏位”白嫖一位精度。
23 位存储 + 1 位隐藏 = 24 位有效精度(单精度) 52 位存储 + 1 位隐藏 = 53 位有效精度(双精度)
非规格化数(Denormalized Numbers)
如果只允许规格化形式,最接近 0 的就是 (单精度),再小的数就会下溢到 0,造成精度断层。
IEEE 754 用阶码全 0 的特殊编码表示非规格化数,此时隐藏位变为 0:
这样就能平滑过渡到 0(gradual underflow)。
浮点数的加减运算
浮点数加减不能直接对尾数做运算——阶码不同相当于”数量级”不同。需要经过以下步骤:
五步流程
第一步:对阶(Align Exponents)
原则:小阶向大阶看齐。阶码小的那个数的尾数右移,阶码增大。
为什么小阶向大阶?若大阶向小阶看齐,尾数左移会丢失高位——那可是最显著的数字。
阶码小的尾数右移 位,阶码同步增加到与另一个相同。
第二步:尾数加减
对阶完成后,两个尾数直接做加减(此时阶码相同了)。
第三步:规格化
运算结果可能不再满足规格化形式。两个极端情况:
- 尾数溢出:和 (如 )→ 尾数右移 1 位,阶码 +1
- 尾数过小:结果高位有多个 0(如 )→ 尾数左移直到最高位为 1,阶码同步减小
第四步:舍入(Rounding)
规格化后,尾数可能超出可存储的位数,需要舍入。详见 IEEE 754 舍入与精度。
第五步:溢出判断
- 阶码超出最大表示范围 → 上溢(overflow),通常变为
- 阶码低于最小表示范围 → 下溢(underflow),变为 0 或非规格化数
完整示例
计算 (十进制)在单精度下的过程:
| 步骤 | ||
|---|---|---|
| 真值 | ||
| 符号/阶码/尾数 | s=0, E=0, M=1.1 | s=0, E=-1, M=1.1 |
1. 对阶:B 的阶码 -1 小于 A 的阶码 0,差 = 1。B 的尾数右移 1 位:
2. 尾数加:
3. 规格化:尾数 ,右移 1 位:
4. 舍入: 在 23 位精度内,无变化。
5. 结果: ✓
IEEE 754 格式
IEEE 754 是浮点数的事实标准(1985 年制定,2008/2019 年修订),几乎所有的 CPU 和 GPU 都遵循。
两种常用精度
| 精度 | 总位数 | 符号 | 阶码 | 尾数(存储) | 尾数(有效) | 偏置值 |
|---|---|---|---|---|---|---|
| 单精度 (float) | 32 | 1 | 8 | 23 | 24 | 127 |
| 双精度 (double) | 64 | 1 | 11 | 52 | 53 | 1023 |
偏置值通式:( 为阶码位数)
阶码编码与特殊值
阶码使用保留编码来区分不同情况:
| 阶码(移码) | 尾数 | 含义 |
|---|---|---|
= 0 | ±0 | |
≠ 0 | 非规格化数(denormalized) | |
| ~ | 任意 | 规格化数(normalized) |
= 0 | ±∞(无穷大) | |
≠ 0 | NaN(Not a Number) |
特殊值详解
零(±0)
IEEE 754 区分 +0 和 -0。两者在比较时视为相等,但在除法中行为不同(,)。
无穷大(±∞)
上溢时产生。 可以参与运算:
- (未定义)
NaN(Not a Number)
非法操作的结果:、、、。
NaN 有两种:
- Signaling NaN (sNaN):触发异常,用于调试
- Quiet NaN (qNaN):静默传播,不触发异常
关键特性:,任何与 NaN 的比较都返回 false。检查 NaN 只能用
isnan()。
数值范围
| 精度 | 最小正规格化数 | 最大正数 | 最小正非规格化数 |
|---|---|---|---|
| 单精度 | |||
| 双精度 |
IEEE 754 舍入与精度
为什么需要舍入?
浮点数的尾数位数有限。运算结果的有效位数如果超过存储容量,就必须舍入。IEEE 754 规定了四种舍入模式。
四种舍入模式
| 模式 | 行为 | 应用 |
|---|---|---|
| 舍入到最近偶数 (roundTiesToEven) | 舍入到最近值;正好在中间时取最低位为 0 的值(偶数) | 默认模式,最常用 |
| 向 0 舍入 (roundTowardZero) | 直接截断,绝对值变小 | C 语言的类型转换 |
| 向 +∞ 舍入 (roundTowardPositive) | 总是向上取 | 区间算术上界 |
| 向 -∞ 舍入 (roundTowardNegative) | 总是向下取 | 区间算术下界 |
舍入到最近偶数为啥是默认?
普通的”四舍五入”在大量运算中会有统计偏差——中间值总是向上舍,导致结果系统性偏大。而最近偶数模式在中间值上有一半情况向上、一半情况向下,无偏。
| 需要舍入的值 | 舍入到最近偶数 |
|---|---|
| 1.4 → 1 | ✓(更近) |
| 1.6 → 2 | ✓(更近) |
| 1.5 → 2 | 偶数(1 是奇数,2 是偶数) |
| 2.5 → 2 | 偶数(3 是奇数,2 是偶数) |
保护位、舍入位、粘贴位(Guard, Round, Sticky)
硬件用额外的三个位来确保舍入正确:
| 位 | 含义 |
|---|---|
| Guard (G) | 尾数最低位之后的第一位 |
| Round (R) | Guard 位之后的第二位 |
| Sticky (S) | Round 之后所有位的逻辑或(只要后面有任何 1,S 就是 1) |
舍入决策:
- 若 G=0 → 截断
- 若 G=1 且 R=S=0 → 最近偶数(看最低位)
- 若 G=1 且 (R=1 或 S=1) → 向上进位
C 语言中的浮点数类型
类型映射
| C 类型 | IEEE 754 对应 | 位数 | 有效数字(十进制) |
|---|---|---|---|
float | 单精度 | 32 | ~7 位 |
double | 双精度 | 64 | ~15-16 位 |
long double | 平台相关 | x86: 80; ARM: 128 | 取决于实现 |
默认类型:C 中浮点字面量(如
3.14)默认为double。赋给float时发生隐式转换。
float.h 关键宏
#include <float.h>
// floatFLT_MAX // ≈ 3.402823e+38 最大正数FLT_MIN // ≈ 1.175494e-38 最小正规格化数FLT_EPSILON // ≈ 1.192093e-07 1.0 与下一个可表示数之差FLT_DIG // 6 十进制有效位数
// doubleDBL_MAX // ≈ 1.797693e+308DBL_MIN // ≈ 2.225074e-308DBL_EPSILON // ≈ 2.220446e-16DBL_DIG // 15
// true minimum (denormalized)FLT_TRUE_MIN // ≈ 1.401298e-45 最小正非规格化数DBL_TRUE_MIN // ≈ 4.940656e-324
FLT_EPSILON不是最小正数,而是1 和下一个可表示浮点数之间的距离——即浮点数的”分辨率”。
常见陷阱
1. 不要用 == 比较浮点数
float a = 0.1f + 0.2f; // 实际可能是 0.30000001if (a == 0.3f) // 可能是 false! printf("equal\n");
// ✅ 正确做法:用容差if (fabs(a - 0.3f) < 1e-6) printf("approximately equal\n");2. 大数吃小数(Catastrophic Cancellation)
float big = 1.0e8f;float small = 1.0f;float sum = big + small; // sum 仍然是 1.0e8f// small 被"吞掉了",因为单精度只有约 7 位有效数字当两个数数量级差超过有效位数时,较小的数对结果完全无贡献。
3. 结合律不成立
浮点加减不满足结合律:
float a = 1.0e8f, b = -1.0e8f, c = 1.0f;(a + b) + c // = 0 + 1 = 1 ✓a + (b + c) // = 1.0e8 - 1.0e8 = 0 ✗编译器不能随意重排浮点运算顺序(除非开启
-ffast-math)。
4. 浮点数到整数的截断
int x = (int)3.14f; // x = 3(向 0 截断)int y = (int)-3.14f; // y = -3(向 0 截断,不是向下取整!)


