浮点数在内存里长什么样
2021 年 6 月 20 日 · 2 分钟读完
整数(不管正负)通过进制转换和补码就能得到对应的二进制。带小数点的数怎么存?现在几乎所有编译器和 CPU 都用 IEEE 754 标准,也就是二进制的科学计数法。
二进制科学计数法
十进制的科学计数法是 -5.1 = -5.1 × 10^0,二进制的写成 (-1)^S × 1.M × 2^E,分三段存:
| 类型 | 总位数 | 符号位 S | 阶码 E | 尾数 M | 指数偏移量 |
|---|---|---|---|---|---|
| float | 32 | 1 | 8 | 23 | 127 |
| double | 64 | 1 | 11 | 52 | 1023 |

- 符号位:0 表示正,1 表示负。
- 阶码:指数可正可负,但存的时候不用补码,而是加上一个偏移量变成无符号数(移码)。float 的偏移量是 127,指数 2 存成
2 + 127 = 129 = 1000 0001;double 的偏移量是 1023。 - 尾数:小数点后面的部分。
以 float 的 -5.1 为例
第一步:忽略符号,转成二进制
整数部分 5 → 101
小数部分 0.1 → 乘二取整 → 0.0001 1001 1001 1001 ...(1001 无限循环)
合起来 → 101.0001 1001 1001 1001 ...
第二步:规格化
把小数点挪到第一个 1 后面:
101.0001 1001 1001 ... = 1.0100 0110 0110 0110 ... × 2^2
规格化以后小数点左边一定是 1,所以干脆不存它,省下的一位留给尾数,精度多了一位。
第三步:拼出三段
符号位:负数 → 1
阶码: 2 + 127 = 129 → 1000 0001
尾数: 取小数点后 23 位 → 0100 0110 0110 0110 0110 011
拼起来:1 | 1000 0001 | 0100 0110 0110 0110 0110 011
按 4 位一组:1100 0000 1010 0011 0011 0011 0011 0011 = 0xC0A33333
0.1 在二进制里是无限循环小数,23 位尾数只能存一个近似值,浮点数存的从来都是近似值,这就是精度丢失的来源。直接读内存验证一下:

自己多写几个小数算一算,再用代码读内存核对,很快就熟了。
延伸
- 不用写 Demo 也能看到这 32 位:C# 里
BitConverter.SingleToInt32Bits(-5.1f).ToString("X8")输出C0A33333(.NET Core 2.0 起可用);double 用BitConverter.DoubleToInt64Bits。注意 Windows 是小端,直接看内存窗口时字节顺序是反的:33 33 A3 C0。 - 为什么
0.1 + 0.2 != 0.3:两边都是近似值,加起来的误差刚好和 0.3 的近似值不一样。比较浮点数要用误差范围:Math.Abs(a - b) < 1e-6。 - 钱和计数别用 float/double:C# 的
decimal是十进制浮点,128 位、28~29 位有效数字,0.1m + 0.2m == 0.3m成立,适合金额;代价是比 double 慢很多。 - float 能精确表示的整数只到 2^24(16777216):再大就会跳着走。PLC 里的 REAL 就是 32 位 float,用它做计数器累加到一千多万后会「加不上去」,计数请用 DINT。
- 几个特殊值:阶码全 0 表示 0 和非规格化数,阶码全 1 表示无穷大(尾数为 0)和 NaN(尾数不为 0)。
double.NaN == double.NaN是 false,判断要用double.IsNaN。
原文首发于 blog.csdn.net