← 全部文章 计算机基础

浮点数在内存里长什么样

2021 年 6 月 20 日 · 2 分钟读完

目录
  1. 二进制科学计数法
  2. 以 float 的 -5.1 为例
  3. 第一步:忽略符号,转成二进制
  4. 第二步:规格化
  5. 第三步:拼出三段
  6. 延伸

整数(不管正负)通过进制转换和补码就能得到对应的二进制。带小数点的数怎么存?现在几乎所有编译器和 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

  1. 符号位:0 表示正,1 表示负。
  2. 阶码:指数可正可负,但存的时候不用补码,而是加上一个偏移量变成无符号数(移码)。float 的偏移量是 127,指数 2 存成 2 + 127 = 129 = 1000 0001;double 的偏移量是 1023。
  3. 尾数:小数点后面的部分。

以 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