目前,绝大多数计算机系统都采用 IEEE 754标准 来表示浮点数。我们将以最常用的 单精度(32位,C/C++中的 float) 为例进行详细讲解。
核心思想:科学计数法(二进制版)
浮点数的存储思路和我们熟悉的科学计数法(如 ( 3.14 × 102 ))一模一样,只不过它使用的是二进制科学计数法。
一个二进制浮点数可以表示为:
( N = (-1)S × M × 2E )
其中:
- S:符号
- M:尾数
- E:指数
IEEE 754标准的核心工作,就是规定在固定的32位或64位空间里,如何存放S、M、E这三个部分。
内存布局(32-bit Float)
一个 float 变量占用 4字节(32位)。这32位被划分为三个部分:
| 1 bit | 8 bits | 23 bits |
|---------------|----------------|-----------------------|
| 符号位 (S) | 指数位 (E) | 尾数位 (M) |
我们来逐一拆解每个部分的意义。
1. 符号位
- 位置:最高位(第31位)。
- 长度:1 bit。
- 含义:
0:表示正数。1:表示负数。
- 这非常简单,它决定了整个数的正负。
2. 指数位
- 位置:第30位到第23位。
- 长度:8 bits。
- 含义:存储指数。但这里有一个非常重要的技巧——偏移码。
为什么需要偏移码?
指数E可以是正数(如 ( 210)),也可以是负数(如 ( 2-5}))。为了省去再用一个符号位来表示指数正负的麻烦,IEEE 754规定了一个偏移值。
对于8位指数(float),这个偏移值是 127。
实际存储的值(我们称为 e)与真实指数(E)的关系是:e = E + 127
- 例子1:如果真实指数 E = 2,那么存储的
e= 2 + 127 = 129。129的二进制是10000001。 - 例子2:如果真实指数 E = -3,那么存储的
e= -3 + 127 = 124。124的二进制是01111100。
这样一来,8位的指数域(e)能表示的范围是 0 ~ 255。通过减去127,真实指数E的范围就变成了 -127 ~ 128。这完美地解决了正负指数的问题。
3. 尾数位
- 位置:第22位到第0位。
- 长度:23 bits。
- 含义:存储尾数 的小数部分。这里也有一个关键技巧——隐含的1。
为什么有隐含的1?
在二进制科学计数法中,我们总是可以将一个数规范化,使其变成 ( 1.xxxx… × 2E) 的形式(注意,是二进制,所以整数部分只能是1)。
例如:十进制数 ( 5.0 ) 的二进制是 ( 101.0 )。科学计数法规范化后是 ( 1.01 × 22)。
请注意 1.01,整数部分的 1 是固定的!既然它永远是1,我们就没有必要浪费一个宝贵的bit去存储它。所以,在23位的尾数域中,我们只存储小数部分,也就是 .01。
这个隐含的、没有存储的“1”,被称为“隐藏位”或“前导1”。
所以,当我们从内存中读取尾数位时,实际表示的尾数 M = 1 + (23位小数部分对应的十进制值)。
完整转换流程:以 -6.625 为例
让我们亲手将 -6.625 这个十进制数转换为 IEEE 754 的 float 格式。
步骤1:确定符号 S
这是一个负数,所以 S = 1。
步骤2:将绝对值转换为二进制
- 整数部分:6 的二进制是
110。 - 小数部分:0.625 的二进制是多少? 0.625 × 2 = 1.25 → 取1,剩0.25;0.25 × 2 = 0.5 → 取0;0.5 × 2 = 1.0 → 取1,剩0。所以是
.101。 - 所以,( 6.62510 = 110.101{2})。
步骤3:规范化二进制数
将 110.101 写成 ( 1.xxxx × 2E) 的形式。
110.101 = ( 1.10101 × 22) (相当于小数点向左移动了2位)
所以:
- 尾数部分 M =
1.10101(记住,我们只存小数部分) - 真实指数 E = 2
步骤4:处理指数位(加偏移)
e = E + 127 = 2 + 127 = 129
129 的二进制是 10000001
步骤5:处理尾数位(取小数部分)
尾数 M = 1.10101
小数部分是 10101
我们需要用23位来表示它,所以在后面补0:
10101 → 1010 1000 0000 0000 0000 000 (补了18个0)
步骤6:组合所有部分
现在我们有:
- S =
1 - E =
10000001 - M =
1010 1000 0000 0000 0000 000
组合起来就是:
1 10000001 10101000000000000000000
为了书写方便,通常写成16进制:
1100 0000 1101 0100 0000 0000 0000 0000 -> 0xC0D4 0000
你可以用在线工具或写一段简单的C代码(printf("%08X\n", *(int*)&yourFloat);)来验证这个结果。
特殊的指数值
指数域 e 的全0和全1被赋予了特殊含义,用来表示一些特殊情况:
| 指数 (e) | 尾数 (M) | 含义 |
|---|---|---|
00000000 | 000...000 | ±0 (正负由符号位S决定) |
00000000 | 非 000...000 | 次正规数。此时隐藏位被视为0,而不是1。用于表示非常接近0的数。 |
00000001 到 11111110 | 任何值 | 正规数。这就是我们上面讨论的正常情况。 |
11111111 | 000...000 | 无穷大 (±∞, 由符号位S决定) |
11111111 | 非 000...000 | NaN。表示“不是一个数字”,例如 sqrt(-1) 或 0.0/0.0 的结果。 |
总结与要点
- 核心公式:Value = (-1)S × (1.M){2} × 2(E-127)
- 三个关键技巧:
- 科学计数法:将数字分解为符号、尾数和指数。
- 偏移码:通过加一个固定值(127)来存储指数,避免为指数再设符号位。
- 隐藏位:对于正规数,尾数的整数部分1被隐含,不存储,从而在23位中获得了24位的精度。
- 精度问题根源:浮点数在内存中是以离散的、有限的二进制小数表示的。很多在十进制中有限的数(如0.1),在二进制中是无限循环小数,无法被精确表示,只能存储一个近似值。这就是为什么
0.1 + 0.2 != 0.3的根本原因。 - 双精度(double):64位
double的原理完全一样,只是位数不同:- 1位符号位
- 11位指数位(偏移值为1023)
- 52位尾数位

被折叠的 条评论
为什么被折叠?



