将双精度数分配给 C 中的 int 变量的非直观结果

Posted

技术标签:

【中文标题】将双精度数分配给 C 中的 int 变量的非直观结果【英文标题】:Nonintuitive result of the assignment of a double precision number to an int variable in C 【发布时间】:2018-08-07 08:14:12 【问题描述】:

谁能给我一个解释为什么我得到两个不同的 数字,分别。 14 和 15,作为以下代码的输出?

#include <stdio.h>  

int main()

    double Vmax = 2.9; 
    double Vmin = 1.4; 
    double step = 0.1; 

    double a =(Vmax-Vmin)/step;
    int b = (Vmax-Vmin)/step;
    int c = a;

    printf("%d  %d",b,c);  // 14 15, why?
    return 0;

我希望在这两种情况下都获得 15,但似乎我缺少该语言的一些基础知识。

我不确定它是否相关,但我正在 CodeBlocks 中进行测试。但是,如果我在某些在线编译器 (this one for example) 中键入相同的代码行,我会得到两个打印变量的 15 答案。

【问题讨论】:

Same FLT_EVAL_METHOD, different results in GCC/Clang的可能重复 这不是Same FLT_EVAL_METHOD, different results in GCC/Clang 的副本,因为该问题的答案不适用于这个问题。 【参考方案1】:

...为什么我得到两个不同的数字...

除了通常的浮点问题之外,bc 的计算路径以不同的方式到达。 c 的计算方法是先将值保存为double a

double a =(Vmax-Vmin)/step;
int b = (Vmax-Vmin)/step;
int c = a;

C 允许使用更广泛的类型计算中间浮点数学。从&lt;float.h&gt; 中检查FLT_EVAL_METHOD 的值。

除了赋值和强制转换(删除所有额外的范围和精度),...

-1 无法确定;

0 仅根据范围和精度评估所有操作和常量 类型;

1 将 floatdouble 类型的操作和常量评估为 double 类型的范围和精度,评估 long double long double 的范围和精度的运算和常数 类型;

2 评估所有操作和常量的范围和精度 long double 类型。

C11dr §5.2.4.2.2 9

操作reported 2

通过将商保存在double a = (Vmax-Vmin)/step; 中,精度将强制为double,而int b = (Vmax-Vmin)/step; 可以计算为long double

这种细微的差异是由于(Vmax-Vmin)/step(可能计算为long double)被保存为double 而保留为long double。一个为 15(或略高于),另一个略低于 15。int 截断将这种差异放大到 15 和 14。

在另一个编译器上,由于FLT_EVAL_METHOD &lt; 2 或其他浮点特性,结果可能都相同。


从浮点数到int 的转换对于接近整数的数字非常严重。通常更好round()lround()。最佳解决方案视情况而定。

【讨论】:

感谢您的帮助和解释!现在,当我在在线编译器上测试FTL_EVAL_METHOD 时,我得到 15 个变量的“预期”答案,结果为 0。然而,教训是像我这样的菜鸟必须小心乍一看,做这样“简单”的计算:) @GeorgiD With FTL_EVAL_METHOD == 0 我希望b,c 的结果相同,但可能不是 15,而是 14。包括@Steve Summit 在内的许多人建议,在将 FP 转换为int- 这适用于我们所有人,而不仅仅是学习者。【参考方案2】:

这确实是一个有趣的问题,这正是在您的硬件中发生的事情。这个答案给出了 IEEE double 精度浮点数的精确计算,即 52 位尾数加一个隐式位。有关表示的详细信息,请参阅wikipedia article。

好的,所以你先定义一些变量:

double Vmax = 2.9;
double Vmin = 1.4;
double step = 0.1;

二进制中的相应值将是

Vmax =    10.111001100110011001100110011001100110011001100110011
Vmin =    1.0110011001100110011001100110011001100110011001100110
step = .00011001100110011001100110011001100110011001100110011010

如果你计算位,你会看到我给出了第一个被设置的位加上右边的 52 位。这正是您的计算机存储double 的精度。 请注意,step 的值已四舍五入。

现在您对这些数字进行一些数学运算。第一个操作,减法,得到精确的结果:

 10.111001100110011001100110011001100110011001100110011
- 1.0110011001100110011001100110011001100110011001100110
--------------------------------------------------------
  1.1000000000000000000000000000000000000000000000000000

然后你除以step,它已经被你的编译器四舍五入了:

   1.1000000000000000000000000000000000000000000000000000
 /  .00011001100110011001100110011001100110011001100110011010
--------------------------------------------------------
1110.1111111111111111111111111111111111111111111111111100001111111111111

由于step 的四舍五入,结果略低于15。与以前不同,我没有立即四舍五入,因为这正是有趣的事情发生的地方:你的 CPU 确实可以存储比 double 更精确的浮点数,所以四舍五入不会立即发生.

因此,当您将(Vmax-Vmin)/step 的结果直接转换为int 时,您的CPU 会简单地切断小数点之后的位(这就是语言标准定义的隐式double -&gt; int 转换的方式):

               1110.1111111111111111111111111111111111111111111111111100001111111111111
cutoff to int: 1110

但是,如果您首先将结果存储在 double 类型的变量中,则会进行舍入:

               1110.1111111111111111111111111111111111111111111111111100001111111111111
rounded:       1111.0000000000000000000000000000000000000000000000000
cutoff to int: 1111

这正是你得到的结果。

【讨论】:

太好了,每一个关于浮点数的问题都应该有这样的具体例子。 "注意step的值已经四舍五入了。这个四舍五入是语言标准规定的。"。所有 3 个Vmax, Vmin, step 都进行了四舍五入。 step:上。 Vmax, Vmin:下来。这些是四舍五入的例子。 “这种四舍五入是由语言标准规定的。”嗯,查看§5.2.4.2.2 6. 相反,精度和舍入方向/模式是实现定义的行为。各种 FP 标准确实将四舍五入指定为默认舍入模式,但 C 没有。仍然有许多平台符合 IEEE 754 - 或几乎如此。 @chux 如果我错了,请纠正我,但我的印象是,浮点 literals 必须四舍五入到最接近。其他地方的舍入确实是实现定义的,就像存储计算结果时发生的舍入一样。 @cmaster 看起来仍然是 ID 行为。 §6.4.4.2 7“浮动常量的转换时转换应与库函数(例如 strtod)对字符串的执行时转换相匹配,给定适合两种转换的匹配输入、相同的结果格式和默认执行时间舍入”和脚注“库函数的规范建议比浮点常量所需的转换更准确(参见 7.22.1.3)。” C 在这些关于浮动常量的问题上相当松散。 @chux 好的,我已经删除了违规的句子【参考方案3】:

“简单”的答案是那些看似简单的数字 2.9、1.4 和 0.1 在内部都表示为二进制浮点数,而在二进制中,数字 1/10 表示为无限重复的二进制分数 0.00011001100110011。 ..[2] . (这类似于十进制的 1/3 最终是 0.333333333...。)转换回十进制,这些原始数字最终会变成 2.8999999999、1.3999999999 和 0.0999999999。当你对它们进行额外的数学运算时,那些 .0999999999 往往会激增。

另外一个问题是你计算某些东西的路径——无论你是把它存储在特定类型的中间变量中,还是“一次全部”计算它,这意味着处理器可能使用更大的内部寄存器比 double 类型更精确——最终可能会产生重大影响。

底线是,当您将double 转换回int 时,您几乎总是希望舍入,而不是截断。这里发生的事情是(实际上)一个计算路径给了你 15.0000000001,它被截断到 15,而另一个给你 14.999999999,它一直被截断到 14。

另请参阅C FAQ list 中的question 14.4a。

【讨论】:

准确地说,如果 1/10 表示为无限重复的二进制分数0.00011001100110011...(并且如果算术以数学上正确的方式完成),那么将这不是问题,但实际上它被表示为二进制分数被截断为一定数量的数字。 (正如在十进制中,具有无限数字序列的数字 0.333333… 正好是 1/3,但是当截断为有限数字时,我们会得到类似 0.33333333333333 的东西 not 正好是 1/3。)【参考方案4】:

在analysis of C programs for FLT_EVAL_METHOD==2中分析了一个等效问题。

如果FLT_EVAL_METHOD==2:

double a =(Vmax-Vmin)/step;
int b = (Vmax-Vmin)/step;
int c = a;

通过计算b 计算long double 表达式,然后将其截断为int,而对于c,它从long double 求值,将其截断为double,然后截断为int

所以这两个值不是通过相同的过程获得的,这可能会导致不同的结果,因为浮点类型不提供通常的精确算术。

【讨论】:

是的,谢谢让-巴蒂斯特!在@chux 提到FLT_EVAL_METHOD 之后,这也是我从昨天的讨论中了解到的。

以上是关于将双精度数分配给 C 中的 int 变量的非直观结果的主要内容,如果未能解决你的问题,请参考以下文章

快速将双精度转换为 Int64

将双精度舍入到最接近的非次正规表示

将浮点数分配给子集返回 1

将浮点数分配给 long double 时,它​​的值会发生啥变化?

将双精度的 C-Struct 编组为 C# - 正值错误

如何将双精度值从 json 转换为函数