当前位置: 首页 > news >正文

【浮点数存储】double类型注意点

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

文章目录

  • 一、double类型精度
    • 1. 以0.1存储举例
      • 步骤1:十进制0.1转换为二进制
      • 步骤2:拆分IEEE 754的三个部分
      • 步骤3:组合为64位存储
      • 关键结论
    • 2. 关于有效位数与超长浮点数
    • 总结
  • 二、double类型可以精确表示的数
    • 1. 判断方法
    • 2. 总结说明
  • 三、std::setprecision透过现象看本质
    • 问题引入
    • 解答
      • 一、0.1的IEEE 754 double类型二进制存储原理
        • 步骤1:规范化二进制
        • 步骤2:确定符号位、指数位、尾码位
        • 0.1的完整64位IEEE 754存储
      • 二、从64位二进制转换为十进制的过程
        • 具体计算尾码值
        • 整体计算
      • 三、为什么`std::setprecision(60)`能输出超长数字?
      • 四、关键结论
      • 具体拆解这一过程:
      • 关键本质:
  • 四、无限循环有理数(如 1/3)乘以分母后还原
      • 1. 编译器的“代数优化”干预
      • 2. 硬件浮点单元的精度特性
      • 3. 验证:禁用优化后的结果
      • 总结
  • 五、哪些数比实际大,哪些比实际小
    • 问题引入
    • 解答
      • 一、关键纠正:不是“截断”,而是“舍入”
      • 二、以0.1为例:存储值其实比实际值大
      • 三、反例:存储值可能小于实际值(如0.3)
      • 四、总结
    • 如何判断一个浮点数在IEEE754标准下的存储值是大于还是小于实际值?
      • 一、前提:明确“数学真实值”与“可表示值”
      • 二、核心判断依据:二进制展开的“超出部分”与舍入规则
        • 步骤1:将“数学真实值”转换为二进制
        • 步骤2:截取前52位尾码,观察“超出部分”
        • 步骤3:根据“超出部分R”与“中间值M”的关系判断
      • 三、实例:用0.1和0.3验证
        • 例1:0.1的存储值 > 真实值
        • 例2:0.3的存储值 < 真实值
      • 四、特殊情况:“中间值”的舍入(R = M)
      • 五、总结:判断流程


在上篇文章对浮点数存储基础做了说明,文章链接如下:
浮点数存储结构说明
下面结合实际使用写几个注意点:

一、double类型精度

1. 以0.1存储举例

在C++中,double类型遵循IEEE 754双精度浮点数标准,占用64位(8字节)存储空间。这64位被划分为三个部分:符号位(1位)、指数位(11位)和尾数位(52位)。

0.1是十进制小数,需要先转换为二进制,再按照IEEE 754标准编码。

步骤1:十进制0.1转换为二进制

十进制0.1转换为二进制是一个无限循环小数
0.1₁₀ = 0.00011001100110011...₂(循环节为0011

用科学计数法表示(二进制):
1.1001100110011...₂ × 2⁻⁴
(将小数点左移4位,得到整数部分为1的标准形式)

步骤2:拆分IEEE 754的三个部分

IEEE 754双精度格式为:
[符号位(1位)][指数位(11位)][尾数位(52位)]

  1. 符号位
    0.1是正数,符号位为0

  2. 指数位

    • 科学计数法中的指数为 -4(来自2⁻⁴)。
    • IEEE 754用偏移量表示指数(双精度偏移量为1023),因此实际存储的指数值为:
      偏移后指数 = 指数 + 1023 = -4 + 1023 = 1019
    • 将1019转换为11位二进制:1111111011
  3. 尾数位
    科学计数法中1.xxxxxxxx部分(整数位的1被隐含存储,不占用空间)。
    0.1的二进制小数部分是无限循环的001100110011...,尾数位取前52位(需舍入):
    10011001100110011001100110011001100110011001100110011

步骤3:组合为64位存储

将三部分拼接,得到0.1在double中的64位存储:

符号位  指数位(11位)        尾数位(52位)
0      01111111011  10011001100110011001100110011001100110011001100110011

转换为十六进制(每4位二进制对应1位十六进制):
0x3FB999999999999A

锤子在线工具网

关键结论

  • 由于0.1的二进制是无限循环小数,double无法精确存储0.1,只能存储其近似值。
  • 这也是浮点数计算可能出现精度误差的原因(例如0.1 + 0.2 != 0.3)。

十进制小数转换为二进制小数的核心方法是**“乘2取整法”**,即不断将小数部分乘以2,取整数部分作为二进制的一位,直到小数部分为0或达到精度精度要求为止。

下面详细演示0.1转换为二进制的过程:

步骤:将0.1₁₀转换为二进制

  1. 取小数部分0.1,乘以2:
    0.1 × 2 = 0.2 → 整数部分为0(二进制小数第1位:0.0…)
    剩余小数部分:0.2

  2. 用剩余的0.2继续乘以2:
    0.2 × 2 = 0.4 → 整数部分为0(二进制小数第2位:0.00…)
    剩余小数部分:0.4

  3. 用剩余的0.4继续乘以2:
    0.4 × 2 = 0.8 → 整数部分为0(二进制小数第3位:0.000…)
    剩余小数部分:0.8

  4. 用剩余的0.8继续乘以2:
    0.8 × 2 = 1.6 → 整数部分为1(二进制小数第4位:0.0001…)
    剩余小数部分:0.6

  5. 用剩余的0.6继续乘以2:
    0.6 × 2 = 1.2 → 整数部分为1(二进制小数第5位:0.00011…)
    剩余小数部分:0.2

此时发现,剩余的小数部分又回到了0.2(与步骤2相同),这意味着后续会进入循环:

  • 步骤6:0.2×2=0.4 → 整数0(第6位:0.000110…)
  • 步骤7:0.4×2=0.8 → 整数0(第7位:0.0001100…)
  • 步骤8:0.8×2=1.6 → 整数1(第8位:0.00011001…)
  • 步骤9:0.6×2=1.2 → 整数1(第9位:0.000110011…)
  • … 循环往复 …

0.1₁₀转换为二进制是无限循环小数
0.00011001100110011...₂(循环节为0011

因为10和2的最大公约数是2,不是10的倍数,所以十进制的0.1无法用有限位二进制表示,只能无限循环,这也是double无法精确存储0.1的根本原因。

double类型(IEEE 754双精度浮点数)的“15-17位有效数字”是一个统计性结论,具体到某个数字可能是15位、16位或17位,并非固定值。这个范围的根源是二进制与十进制的转换特性,以及double自身的存储结构(53位二进制有效位)。

2. 关于有效位数与超长浮点数

为什么是“15-17位”?核心原因是53位二进制有效位的限制

double的存储结构中,尾数位(小数部分)是52位,加上隐含的“整数位1”(科学计数法中1.xxxx...的整数部分),总共是53位二进制有效位

这53位二进制有效位能表示的最大精度,需要转换为十进制有效位来理解:

  • 二进制有效位与十进制有效位的转换公式:十进制有效位数 ≈ 二进制有效位数 × log10(2)
  • 计算:53 × log10(2) ≈ 53 × 0.3010 ≈ 15.95

即53位二进制有效位大约对应15.95位十进制有效位。这就是“15-17位”的理论基础——实际数字的有效位会围绕这个值波动。

为什么是“范围”而非固定值?

因为二进制与十进制的“精度对齐”不是严格一一对应的。具体来说:

  1. 有些二进制浮点数转换为十进制后,能精确表示17位有效数字;
  2. 有些只能精确到15位;
  3. 大多数情况是16位(因为15.95更接近16)。

举例说明:

  • 情况1:恰好能表示17位有效数字
    例如8.98846567431158000(一个特殊的二进制浮点数),其double存储值转换为十进制后,前17位有效数字完全准确。

  • 情况2:只能表示15位有效数字
    例如0.1double存储值是0.10000000000000000555...,其有效数字从“1”开始,前15位是100000000000000(准确),但第16位开始出现误差(实际是5,而非0)。

  • 多数情况:16位有效数字
    大部分十进制数转换为double后,前16位有效数字是准确的,第17位可能存在±1的误差。

总结

double的“15-17位有效数字”是由其53位二进制有效位决定的:

  • 理论基础:53位二进制≈15.95位十进制有效位;
  • 实际表现:因二进制与十进制的转换特性,不同数字的精确有效位数会在15-17之间波动(多数为16位)。

因此,double不能保证“固定15位”或“固定17位”,但可以认为**“在绝大多数情况下,前16位有效数字是准确的,误差通常出现在第17位”**。这也是工程中常说“double有16位有效精度”的原因。

总结

之前一直有一个误区:之前一直听说double类型精度是十几位,所以一直认为只有像一个很长有效位数的浮点数如123.456789147258369159357这种数字double存储时取前面十几位保存,现在看来完全是自己一窍不通,

像0.1这个数字double型就不能够精确表示,任何一个浮点数无论是0.1还是123.456789147258369159357通过那个IEEE754的转化最终能够保证存储下来的数保证15-17位有效数字准确。

二、double类型可以精确表示的数

要判断一个数是否能被 double 类型精确表示,需要理解 double 的存储原理:double 只能精确表示分母为 2 的整数次幂的分数(即形如 n/(2^k) 的数,其中 nk 是整数)。对于其他形式的数(如分母含 2 以外的质因数的分数,或无限不循环小数),double 无法精确表示。

1. 判断方法

一个数能被 double 精确表示的充要条件是:

  • 它是整数,且绝对值 ≤ 2^53(因为 double 的尾数位有 53 位精度,超过则无法精确存储);
  • 或者它是分数,且分母可化简为 2^k(即分数的最简形式中,分母仅含质因数 2)。

例如:

  • 可精确表示0.5(=1/2)、0.75(=3/4)、3.125(=25/8)、123456789(≤2^53);
  • 不可精确表示0.1(=1/10,分母含质因数 5)、1/3(分母含质因数 3)、π(无限不循环小数)。

2. 总结说明

  1. 理论规则double 仅能精确表示形如 n/(2^k) 的分数(分母为 2 的幂)和绝对值 ≤ 2^53 的整数。
  2. 使用注意double 类型精度有效位数15-17位,不仅仅是说一个十进制有效数字超过17位的浮点数无法精确表示,还有形如0.1这种十进制有效位数在17位之内,但根据IEEE754标准本身就不能被精确表示的数。

三、std::setprecision透过现象看本质

问题引入

  • 执行下面代码
#include <iostream>
#include <iomanip>
using namespace std;int main() {double true_value2 = 0.6;std::cout << "数学真实值:         "  << true_value2 << "\n";std::cout << "数学真实值5 :       " << std::setprecision(5) << true_value2 << "\n";std::cout << "数学真实值10:       " << std::setprecision(10) << true_value2 << "\n";std::cout << "数学真实值15:       " << std::setprecision(15) << true_value2 << "\n";std::cout << "数学真实值20:       " << std::setprecision(20) << true_value2 << "\n";std::cout << "数学真实值30:       " << std::setprecision(30) << true_value2 << "\n";std::cout << "数学真实值40:       " << std::setprecision(40) << true_value2 << "\n";std::cout << "数学真实值50:       " << std::setprecision(50) << true_value2 << "\n";std::cout << "数学真实值60:       " << std::setprecision(60) << true_value2 << "\n";
}
  • 结果

数学真实值: 0.6
数学真实值5 : 0.6
数学真实值10: 0.6
数学真实值15: 0.6
数学真实值20: 0.5999999999999999778
数学真实值30: 0.599999999999999977795539507497
数学真实值40: 0.5999999999999999777955395074968691915274
数学真实值50: 0.59999999999999997779553950749686919152736663818359
数学真实值60: 0.59999999999999997779553950749686919152736663818359375

  • 疑问1:为什么设置多少有效位数就能显示出多少有效位数?
  • 疑问2:说double类型精度是15-17位有效位数,17位后面的数字怎么来的?随机填充的吗?

解答

以0.1存储及打印显示为例,给与以下说明:

一、0.1的IEEE 754 double类型二进制存储原理

0.1的十进制无法被double精确表示,因为其二进制是无限循环小数
0.1₁₀ = 0.00011001100110011...₂(循环节是0011)。

IEEE 754 double类型会将其规范化并截断为53位有效数字(1位隐藏位+52位尾码),具体步骤如下:

步骤1:规范化二进制

0.0001100110011...₂规范化为1.xxxx… × 2ⁿ的形式:
0.0001100110011...₂ = 1.1001100110011...₂ × 2⁻⁴
(小数点左移4位,指数为-4)。

步骤2:确定符号位、指数位、尾码位
  • 符号位:0(正数);
  • 指数位:IEEE 754 double的指数采用“偏移值”存储,偏移量为1023。因此指数-4的存储值为1023 + (-4) = 1019,二进制为01111111011
  • 尾码位:取规范化后小数点后的52位(因隐藏位默认是“1”,无需存储)。由于0.1的二进制是无限循环的,这里会进行舍入截断,最终52位尾码为:
    10011001100110011001100110011001100110011001100110011
0.1的完整64位IEEE 754存储
符号位(1)  指数位(11)          尾码位(52)
0          01111111011        10011001100110011001100110011001100110011001100110011

二、从64位二进制转换为十进制的过程

这个64位二进制对应的实际值是一个确定的近似值,转换为十进制的计算方式是:
值 = (-1)^符号位 × (1 + 尾码值) × 2^(指数值 - 1023)

代入0.1的存储:

  • 符号位=0 → (-1)^0 = 1
  • 尾码值=sum(尾码位第i位 × 2^(-i)) (i从1到52);
  • 指数值=1019 → 2^(1019 - 1023) = 2^(-4)
具体计算尾码值

尾码位是100110011...(循环的10011),计算其对应的十进制分数:

尾码值 = 1×2^(-1) + 0×2^(-2) + 0×2^(-3) + 1×2^(-4) + 1×2^(-5) + ... = 0.1001100110011...₂(二进制分数)= 1/2 + 0 + 0 + 1/16 + 1/32 + 1/512 + ... (十进制展开)

最终,1 + 尾码值 ≈ 1.6000000000000000888...(十进制)。

整体计算

值 = 1 × 1.6000000000000000888... × 2^(-4) = 1.6000000000000000888... / 16 ≈ 0.1000000000000000055511151231257827021181583404541015625

三、为什么std::setprecision(60)能输出超长数字?

这些超长数字不是随机填充,而是从64位二进制存储中精确转换的结果:

  • 64位二进制是一个确定的数值(由符号位、指数位、尾码位唯一确定);
  • 当转换为十进制时,这个二进制值对应唯一一个十进制小数(可能很长);
  • setprecision(60)只是将这个完整的十进制转换结果输出,而不会截断或随机填充。

四、关键结论

  1. 52位尾码对应唯一的十进制值:52位尾码+1位隐藏位+指数位共同确定了一个唯一的二进制值,转换为十进制后是一个固定的(可能很长的)小数,不是随机的。
  2. 超长数字的意义:超过15~17位的数字是转换后的“副产品”,它们是确定的,但没有实际精度意义——因为原数(0.1)的double存储本身就是近似值,这些数字只是近似值的精确十进制表达,不反映真实值的精度。
  3. 总结std::setprecision(60)输出的超长数字是64位二进制的精确十进制转换结果,完全确定,但只有前15~17位有效数字有意义,后续数字是近似存储的“痕迹”,而非随机填充。

简单说:double存储的0.1是一个固定的近似值(二进制确定),转换为十进制后自然会得到一长串固定的数字,setprecision(60)只是如实展示了这个转换结果而已。

“将十进制0.1转换为IEEE 754标准的double类型二进制码时,因0.1的二进制是无限循环小数,无法被有限位尾码精确表示,因此存在精度损失;但生成的IEEE 754二进制码对应一个确定的浮点数**,这个浮点数与二进制码之间是双向精确转换的关系(即二进制码可唯一确定浮点数,浮点数也可唯一反推二进制码)。”**

具体拆解这一过程:

  1. 0.1 → IEEE 754二进制码:存在精度损失
    十进制0.1的二进制是无限循环小数(0.0001100110011...₂),而IEEE 754 double的尾码只有52位(加隐藏位共53位),必须通过舍入截断为有限位。这一步会丢失部分信息,导致存储的二进制码对应的浮点数不等于数学上的0.1,即存在精度损失。

  2. IEEE 754二进制码 → 浮点数:精确转换
    64位二进制码(1位符号位+11位指数位+52位尾码位)是一个唯一确定的数值。通过公式值 = (-1)^符号位 × (1 + 尾码值) × 2^(指数-1023),可以精确计算出其对应的十进制浮点数(例如0.1的存储对应的浮点数是0.1000000000000000055511151231257827021181583404541015625)。这个转换是无歧义、无损失的。

  3. 浮点数 → IEEE 754二进制码:精确反推
    对于任何一个IEEE 754 double类型的浮点数(即符合其存储规则的数值),都可以通过规范化、提取符号/指数/尾码的方式,唯一确定其64位二进制码。这一过程同样无歧义、无损失

关键本质:

IEEE 754标准定义的是**“浮点数与二进制码之间的精确映射规则”**,但这一规则本身无法改变“有限位无法表示无限循环小数”的数学事实。因此:

  • 当原数(如0.1)无法被有限位表示时,转换为二进制码的过程必然损失精度;
  • 但二进制码与它所对应的浮点数之间是“一一对应、精确转换”的关系,这是IEEE 754标准的核心特性(保证了不同系统/设备之间浮点数的一致性)。

这就像“用有限位数的十进制小数表示π(3.1415926…)会损失精度,但一旦确定用3.14159来表示,这个数与它的十进制数字串‘3.14159’之间是精确对应的”。

四、无限循环有理数(如 1/3)乘以分母后还原

#include <iostream>
#include <iomanip>
using namespace std;int main() {double a = 1.0;double b = 3.0;double c = a / b;double d = c * b;// 输出20位有效数字cout << "c = " << setprecision(20) << c << endl;cout << "d = " << setprecision(20) << d << endl;// 直接判断是否等于1.0cout << "d == 1.0 ? " << (d == 1.0 ? "true" : "false") << endl;return 0;
}

输出结果:

c = 0.33333333333333331483
d = 1
d == 1.0 ? true

豆包解释:
这种情况确实可能发生,这与编译器优化、浮点运算单元(FPU)的硬件特性以及浮点数运算的特殊性有关。具体可以从以下角度解释:

1. 编译器的“代数优化”干预

现代编译器(如GCC、Clang、MSVC)会对代码进行代数等价变换优化。当编译器检测到 d = (a / b) * b 这种形式时,可能会直接将其简化为 d = a(因为从数学上 (a/b)*b = a),完全跳过实际的除法和乘法运算。

这种优化会导致:

  • 即使 a/b 存在精度误差,编译器也直接让 d = a(即1.0),因此 d == 1.0 会返回 true
  • 优化与否取决于编译器设置(如是否开启 -O0 禁用优化,或 -O2 启用优化)。

2. 硬件浮点单元的精度特性

CPU的浮点运算单元(如x86的FPU)在处理某些运算时,可能会使用更高精度的临时寄存器(如80位扩展精度)来存储中间结果。

例如:

  • 计算 c = 1.0 / 3.0 时,可能在高精度寄存器中保留了更接近真实值的近似。
  • 乘以3.0时,高精度下的误差可能被“抵消”,最终结果恰好等于1.0(尤其是在运算链较短时)。

3. 验证:禁用优化后的结果

你可以尝试在禁用编译器优化的情况下重新编译(如GCC使用 -O0 选项),此时编译器通常会严格按照代码逻辑执行,不再进行代数简化。

例如,使用 -O0 编译后,代码更可能输出:

c = 0.33333333333333331483
d = 0.99999999999999988898
d == 1.0 ? false

这说明优化与否对结果有直接影响。

总结

  • 你观察到 d == 1.0true,很可能是编译器优化或硬件精度特性导致的特殊情况,而非浮点数运算的普遍规律。
  • 这种结果不具备通用性:换用不同编译器、不同优化级别或不同硬件,可能会得到相反的结果。
  • 开发中仍需遵守浮点数比较的最佳实践:用“差值小于极小值”(如 abs(d - 1.0) < 1e-15)来判断近似相等,而非直接使用 ==

例如,更稳健的比较方式是:

const double epsilon = 1e-15;  // 极小值,根据精度需求调整
cout << "d ≈ 1.0 ? " << (abs(d - 1.0) < epsilon ? "true" : "false") << endl;

五、哪些数比实际大,哪些比实际小

问题引入

  • 执行下面代码
#include <iostream>
#include <iomanip>
using namespace std;int main() {std::cout << "数学真实值0.1:      " << std::setprecision(100) << 0.1 << "\n";std::cout << "数学真实值0.2:      " << std::setprecision(100) << 0.2 << "\n";std::cout << "数学真实值0.3:      " << std::setprecision(100) << 0.3 << "\n";std::cout << "数学真实值0.4:      " << std::setprecision(100) << 0.4 << "\n";std::cout << "数学真实值0.5:      " << std::setprecision(100) << 0.5 << "\n";std::cout << "数学真实值0.6:      " << std::setprecision(100) << 0.6 << "\n";std::cout << "数学真实值0.7:      " << std::setprecision(100) << 0.7 << "\n";std::cout << "数学真实值0.8:      " << std::setprecision(100) << 0.8 << "\n";std::cout << "数学真实值0.9:      " << std::setprecision(100) << 0.9 << "\n";
}
  • 结果

数学真实值0.1: 0.1000000000000000055511151231257827021181583404541015625
数学真实值0.2: 0.200000000000000011102230246251565404236316680908203125
数学真实值0.3: 0.299999999999999988897769753748434595763683319091796875
数学真实值0.4: 0.40000000000000002220446049250313080847263336181640625
数学真实值0.5: 0.5
数学真实值0.6: 0.59999999999999997779553950749686919152736663818359375
数学真实值0.7: 0.6999999999999999555910790149937383830547332763671875
数学真实值0.8: 0.8000000000000000444089209850062616169452667236328125
数学真实值0.9: 0.90000000000000002220446049250313080847263336181640625

解答

对于无法被double精确表示的数字(如0.1),其IEEE 754尾码的确定不是简单的“截断”,而是遵循**“就近舍入”(round to nearest)规则**——这意味着存储的数值可能大于实际值,也可能小于实际值,并非“一定小于”。

一、关键纠正:不是“截断”,而是“舍入”

IEEE 754标准对尾码的处理不是直接截断超出52位的部分,而是根据第53位及以后的二进制位(“截断后剩余的部分”)来决定最终尾码:

  • 若剩余部分小于“中间值”(即第53位为0,或第53位为1但后续全为0且前52位尾码最后一位为0),则向“较小的可表示值”舍入(类似截断,存储值偏小);
  • 若剩余部分大于“中间值”(即第53位为1且后续有非0位),则向“较大的可表示值”舍入(存储值偏大);
  • 若剩余部分恰好等于“中间值”(第53位为1且后续全为0),则向“尾码最后一位为偶数”的方向舍入(可能偏大或偏小)。

二、以0.1为例:存储值其实比实际值大

0.1的十进制对应的二进制是无限循环小数:
0.1₁₀ = 0.0001100110011001100110011001100110011001100110011001100110011...₂

当转换为double(52位尾码)时,前52位尾码为:
1001100110011001100110011001100110011001100110011001(第1到52位)

第53位及以后的位是10011...(循环节继续),这部分大于中间值(因为第53位为1,且后续有非0位)。根据舍入规则,需要向“较大的可表示值”舍入——最终存储的尾码会比“直接截断”多1(即第52位加1,进位后得到最终尾码)。

因此,0.1的double存储值为:
0.1000000000000000055511151231257827021181583404541015625
这个值大于真实的0.1(0.10000000000000000000…)。

三、反例:存储值可能小于实际值(如0.3)

0.3的二进制是0.010011001100110011001100110011001100110011001100110011...₂,前52位尾码后,剩余部分小于中间值,因此舍入后存储值为:
0.299999999999999988897769753748434595763683319091796875
这个值小于真实的0.3

四、总结

  • 对无法精确表示的数字,IEEE 754尾码的确定是舍入而非“截断”;
  • 存储值与实际值的大小关系取决于“超出52位的部分”:
    • 剩余部分>中间值 → 存储值大于实际值(如0.1);
    • 剩余部分<中间值 → 存储值小于实际值(如0.3);
  • “截断”只是舍入的一种特殊情况(仅当剩余部分足够小时发生),不能代表IEEE 754的普遍规则。

这也是为什么浮点数误差方向不确定的核心原因——舍入规则会根据数值本身的二进制特性动态调整。

如何判断一个浮点数在IEEE754标准下的存储值是大于还是小于实际值?

要判断一个浮点数在IEEE 754标准下的存储值(即doublefloat实际存储的近似值)与“数学真实值”的大小关系,核心是分析该浮点数的二进制展开特性以及IEEE 754的舍入规则。具体可按以下步骤判断:

一、前提:明确“数学真实值”与“可表示值”

  • 数学真实值:指该数的精确数学定义(如0.1的真实值是1/10,0.3的真实值是3/10)。
  • 可表示值:指IEEE 754标准中能被精确存储的浮点数(即二进制有限小数,且尾码长度不超过52位(double)或23位(float))。

二、核心判断依据:二进制展开的“超出部分”与舍入规则

对于无法被IEEE 754精确表示的浮点数(即其二进制是无限循环小数),存储值的大小取决于其二进制展开中超出尾码位数的部分与“中间值”的比较。以double类型(52位尾码)为例:

步骤1:将“数学真实值”转换为二进制

首先将目标数(如0.1、0.3)转换为二进制小数。若二进制是有限小数(如0.5=0.1₂,0.25=0.01₂),则它可被double精确表示,存储值等于真实值。

若二进制是无限循环小数(如0.1=0.0001100110011…₂,循环节“0011”),则无法被精确表示,需进一步分析。

步骤2:截取前52位尾码,观察“超出部分”

IEEE 754 double的尾码固定为52位(加隐藏位共53位有效数字)。对于无限二进制小数,需截取其前52位作为“基础尾码”,并观察第53位及以后的“超出部分”(即被舍入的部分)。

  • 记“超出部分”为R(即第53位及以后的二进制数值);
  • 定义“中间值”为M = 2^(-53)(即第53位为1、后续全为0的二进制值,这是“舍”与“入”的临界点)。
步骤3:根据“超出部分R”与“中间值M”的关系判断

IEEE 754的舍入规则是“就近舍入”(round to nearest),具体:

  • R > M:存储值会向上舍入(即向更大的可表示值靠近),因此存储值大于真实值;
  • R < M:存储值会向下舍入(即向更小的可表示值靠近),因此存储值小于真实值;
  • R = M(即第53位为1,后续全为0):此时向“尾码最后一位为偶数”的方向舍入(可能大于或小于真实值,取决于尾码)。

三、实例:用0.1和0.3验证

例1:0.1的存储值 > 真实值
  • 真实值:0.1 = 1/10,二进制是无限循环小数:0.00011001100110011...₂(循环节“0011”)。
  • 二进制展开截取:前52位尾码后,第53位及以后的“超出部分R”是00110011...(循环节继续)。
  • 比较R与MR的二进制值约为0.00110011...₂ × 2^(-52),其数值**大于M=2(-53)**(因循环节“0011”的累积值超过0.5×2(-52))。
  • 结论:存储值向上舍入,因此double存储的0.1(≈0.10000000000000000555)大于真实值0.1。
例2:0.3的存储值 < 真实值
  • 真实值:0.3 = 3/10,二进制是无限循环小数:0.010011001100110011...₂(循环节“0011”)。
  • 二进制展开截取:前52位尾码后,第53位及以后的“超出部分R”是00110011...(循环节继续)。
  • 比较R与MR的数值**小于M=2(-53)**(因累积值不足0.5×2(-52))。
  • 结论:存储值向下舍入,因此double存储的0.3(≈0.2999999999999999889)小于真实值0.3。

四、特殊情况:“中间值”的舍入(R = M)

当“超出部分R”恰好等于中间值M=2^(-53)(即第53位为1,后续全为0),此时存储值的方向由尾码最后一位的奇偶性决定:

  • 若前52位尾码的最后一位是偶数(0):存储值不变(向下舍入,存储值小于真实值);
  • 若前52位尾码的最后一位是奇数(1):存储值加1(向上舍入,存储值大于真实值)。

例如:某数的二进制展开中,前52位尾码最后一位是1,且R=M,则存储值会向上舍入(更大)。

五、总结:判断流程

  1. 将“数学真实值”转换为二进制,判断是否为无限循环小数(若为有限小数,存储值=真实值);
  2. 对无限二进制小数,截取前52位尾码,计算“超出部分R”;
  3. 比较R与中间值M=2^(-53):
    • R > M → 存储值 > 真实值;
    • R < M → 存储值 < 真实值;
    • R = M → 看尾码最后一位奇偶性(偶则小,奇则大)。

通过这个逻辑,可准确判断任意浮点数的存储值与真实值的大小关系。核心是抓住“二进制超出部分”与舍入规则的关联——这也是IEEE 754浮点数误差方向的本质。


http://www.lryc.cn/news/617621.html

相关文章:

  • nginx 设置二级目录-实战
  • 【LLM】OpenAI开源GPT级模型,120B及20B参数GPT-OSS
  • SQL中BETWEEN与IN的差异详解
  • 读《精益数据分析》:媒体内容平台全链路梳理
  • 【数据分析】调控网络分析:调节因子在肿瘤样本中的表达相关性与生存效应分析
  • 【k8s】k8s安装与集群部署脚本
  • 网络性能优化:Go编程视角 - 从理论到实践的性能提升之路
  • 定制化4G专网架构,满足多行业专属需求
  • 5G NR NTN 在 PHY 层和 MAC 层实现 OAI
  • PCB批量线路板厂家有哪些?
  • 2025面试题——(12)
  • Vibe Coding 自然语言驱动 AI 编程方式
  • Redis类型之Hash
  • AI产品经理手册(Ch12-16)AI Product Manager‘s Handbook学习笔记
  • Vue 中的 Class 与 Style 绑定详解1
  • lesson35:数据库深度解析:从概念到MySQL实战学习指南
  • 面试实战 问题二十三 如何判断索引是否生效,什么样的sql会导致索引失效
  • 【排序算法】⑥快速排序:Hoare、挖坑法、前后指针法
  • 微信小程序常用 API
  • Seata
  • 小杰python高级(three day)——matplotlib库
  • Spark 优化全攻略:从 “卡成 PPT“ 到 “飞一般体验“
  • Vlanif 实验
  • 第16届蓝桥杯Python青少组_省赛_中/高级组_2025年5月真题
  • 国企社招 | 中国邮政2025年社会招聘开启
  • 腾讯前端面试模拟详解
  • Java 之抽象类和接口
  • AIStarter修复macOS 15兼容问题:跨平台AI项目管理新体验
  • docker是什么以及镜像命令详解
  • C++模板的补充