在C语言中将整数转换为浮点数时出现奇怪的行为

22

我对以下C程序的输出有疑问。我尝试使用Visual C++ 6.0和MinGW32(gcc 3.4.2)编译它。

#include <stdio.h>

int main() {
    int x = 2147483647;
    printf("%f\n", (float)2147483647);
    printf("%f\n", (float)x);
    return 0;
}

输出结果为:

2147483648.000000
2147483647.000000

我的问题是:为什么这两行不同?当您将整数值2147483647转换为IEEE 754浮点格式时,它会被近似为2147483648.0。因此,我预期这两行都将等于2147483648.000000。

编辑:值"2147483647.000000"不能是单精度浮点值,因为数字2147483647在IEEE 754单精度浮点格式中无法准确表示,会造成精度损失。


6
看起来是编译器相关的。ideone 给出相等的数字。MinGW GCC 4.5.2 给出了和你的一样的结果。 - Eugene Sh.
4
我认为这取决于编译器。对于gcc 4.8.2,在两种情况下都显示2147483648.000000。 - MrTambourineMan
6
一个优化错误。在mingw/gcc-3.4.2和vs6这两个编译器上的输出结果相同吗?顺便说一句,它们都是旧的 - Deduplicator
1
像其他人说的那样,似乎是编译器/优化的 bug。使用带有 -O2-O3 的 gcc 4.9 会为两者都生成 2147483648.000000,而没有任何优化,则会产生与您在帖子中类似的输出。 - P.P
2
在这两种情况下,参数都会从int转换为float(通过强制类型转换),然后从float转换为double(因为printf是一个变参函数)。原则上,由于使用的实现特征,这两个调用都应该打印出2147483648.000000。一些尝试:检查生成的汇编代码(编译器可能已经对其进行了优化,将两个转换合并为一个)。尝试将两个表达式(float)2147483647(float)x存储到float对象中并打印它们的值,或者检查它们的表示形式。 - Keith Thompson
显示剩余5条评论
5个回答

12
在这两种情况下,代码尝试将某些整数类型转换为 float,然后再转换为 double。由于它是传递给可变参数函数的 float 值,因此会出现 double 转换。
检查您的 FLT_EVAL_METHOD 设置,怀疑它的值为 1 或 2(OP 报告至少有一种编译器的值为 2)。这允许编译器评估 float "... 操作和常量的范围和精度" 大于 float
您的编译器对 (float)x 进行了优化,直接将 int 转换为 double 算术运算。这是在运行时的性能提升。 (float)2147483647 是一个编译时转换,编译器优化了 intfloatdouble 的精度,因为性能不是问题。
[编辑2] 有趣的是,C11 标准比 C99 标准更具体,增加了“除赋值和强制类型转换之外...”的内容。这意味着 C99 编译器有时允许 int 直接转换为 double,而不是先经过 float,而 C11 则被修改为明确禁止跳过类型转换。
由于 C11 正式排除了这种行为,现代编译器不应该这样做,但像 OP 这样的旧编译器可能会这样做 - 因此是 C11 标准的一个错误。除非发现其他 C99 或 C89 规范说明有不同的情况,否则似乎可以允许这种编译器行为。
[编辑] 结合 @Keith Thompson、@tmyklebu、@Matt McNabb 的评论,即使具有非零的 FLT_EVAL_METHOD,编译器也应该得出 2147483648.0 ... 的值。因此,要么编译器优化标志明确地覆盖了正确的行为,要么编译器存在角落里的错误。C99dr §5.2.4.2.2 8中,使用浮点数操作且受常规算术转换限制的操作和浮点常量的值被计算为一种格式,其范围和精度可能比类型要求的范围和精度更大。评估格式的使用是由实现定义的FLT_EVAL_METHOD值表征的:
-1 不确定;
0 仅将所有操作和常量计算到类型的范围和精度;
1 将floatdouble类型的操作和常量计算到double类型的范围和精度,将long double类型的操作和常量计算到long double类型的范围和精度;
2 将所有操作和常量计算到long double类型的范围和精度。
C11dr §5.2.4.2.2 9除了赋值和强制类型转换(它们删除所有额外的范围和精度),对带有浮点操作数和受常规算术转换约束的值以及浮点常量进行操作得到的值是被计算为一种格式,其精度和范围可能超过类型所需。评估格式的使用是由实现定义的FLT_EVAL_METHOD值表征的。
-1(与C99相同)
0(与C99相同)
1(与C99相同)
2(与C99相同)

我从来没有完全理解人们是如何从那段话中读出编译器省略赋值和转换是可以的,即使它们删除了所有额外的范围和精度。 - tmyklebu
2
这段话开头是“除了赋值和强制类型转换”,但我们正在讨论的是强制类型转换,因此本段剩余部分不适用(特别是FLT_EVAL_METHOD的相关性)。 - M.M
3
这个答案是错误的。无论 FLT_EVAL_METHOD 的值是什么,显式转换为 float 都会丢失多余精度。这是 C 标准所要求的。如果 MSVC 没有这样做,那么这是编译器中的一个 bug。 - R.. GitHub STOP HELPING ICE
1
@R..:在 VC++6 发布时唯一存在的 C 标准 C89 中,这被禁止在哪里了? - tmyklebu
无论FLT_EVAL_METHOD是什么,编译器在存在显式转换时必须执行到所请求类型的转换。gcc在这方面似乎有一些错误。例如,如果你有double x, y;那么x + y可能会以扩展精度进行评估,但(double)(x + y) _必须_将其转换为双精度。gcc显然在前端删除了强制转换,假设当一个“官方”上被标记为double的值被强制转换为double时不需要转换。即使该值实际上是扩展精度。 - gnasher729
显示剩余6条评论

7
这肯定是编译器的一个漏洞。从C11标准中我们有以下保证(C99类似):
- 类型具有一组可表示的值(隐含) - 所有可由“float”表示的值也可由“double”表示(6.2.5/10) - 将“float”转换为“double”不会更改其值(6.3.1.5/1) - 当int值在float表示的值集合中时,将int强制转换为float给出该值。 - 当int值的大小小于FLT_MAX且int不是可表示的float值时,将int强制转换为float会导致选择下一个最高或最低的float值,选择哪个是实现定义的。(6.3.1.4/2)
这些点中的第三个保证了传递给printf的“float”值不会被默认参数升级修改。
如果2147483647可以通过“float”表示,则“(float)x”和“(float)2147483647”必须给出“2147483647.000000”。
如果2147483647不能通过“float”表示,则“(float)x”和“(float)2147483647”必须给出下一个最高或最低的“float”。它们不必都做出相同的选择。但这意味着不允许打印出“2147483647.000000”,每个值应该是更高或更低的值之一。
注1:理论上可能最低的浮点数为“2147483646.9999999…”,因此当使用printf以6位数字精度显示值时,四舍五入将给出所看到的结果。但在IEEE754中不是这样的,并且您可以轻松地进行实验以排除此可能性。

下一个比 2147483646.9999999... 更小的 float 值可能更接近于 2147483520.0。下一个比 2147483646.9999999... 更小的 double 值可能为 2147483646.9999998... - chux - Reinstate Monica
C99是从1999年开始的。如果您试图将此称为VC++6中的错误,则只有C89是唯一合理的C标准可供参考。(我并不确定这是否是VC++6的错误;这只是一个旧编译器,早在人们意识到破坏浮点代码是不好的之前就已经存在于C和C++实现者中了。) - tmyklebu
@tmyklebu 我没有C89文本的副本,所以我假设它通常没有改变。 - M.M
关于“C11标准,我们有以下保证(C99类似):”,我没有在C99中找到像C11那样的明确保证。 - chux - Reinstate Monica

2

在第一个printf中,整数到浮点数的转换由编译器完成。在第二个printf中,它由C运行时库完成。它们在精度极限处产生相同的答案并没有特别的原因。


编译器也可以执行第二种转换,而不一定要留给C运行时。 - P.P
如果你的代码依赖于像这样的东西相等,那么它就是糟糕的代码。 - Lee Daniel Crocker
我的意思是“编译器在标准的限制下有多少灵活性?” - Oliver Charlesworth
IEEE-754浮点数(单精度,32位)保证转换进出9位数字。这里有10位。 - Lee Daniel Crocker
好的,它保证的是位而不是十进制数字,但我知道你的意思。但是在舍入/截断方面我们有什么(这里必须发生的)?编译器是否允许省略(double)(float)myInt中的舍入? - Oliver Charlesworth
显示剩余9条评论

0

Visual C++ 6.0在上个世纪发布,我相信它比标准C ++更早。 VC ++ 6.0表现出错误行为完全不足为奇。

您还会注意到gcc-3.4.2是从2004年开始的。确实,您正在使用32位编译器。 在x86上,gcc对浮点数的数学计算非常快速和松散。 如果gcc将FLT_EVAL_METHOD设为非零,则这可能在技术上被C标准所证明。


1
“VC6已经过时了,我并不感到惊讶”在这里并没有提供太多有趣的信息或解释。 - Jason C
1
@JasonC:我猜不是,但它并没有声称遵循在它发布几个月后的标准。很难因为所有这些疯狂而责怪它。 - tmyklebu
请注意:"Visual C++ 6.0是上个千年发布的"。 - chux - Reinstate Monica

-1

有些人说这是一个优化错误,但我有点不同意。我认为这是一个合理的浮点精度误差,并且是一个很好的例子,向人们展示浮点数是如何工作的。

http://ideone.com/Ssw8GR

也许楼主可以尝试将我的程序粘贴到您的计算机上,并使用您的编译器进行编译,看看会发生什么。或者尝试:

http://ideone.com/OGypBC

(使用显式浮点转换)。

无论如何,如果我们计算误差,它大约为4.656612875245797e-10,应该被认为是相当精确的。

这可能也与printf的偏好有关。


6
这不是浮点数的工作方式。它不会随意决定四舍五入或不四舍五入。它有明确定义的语义应该被遵循。 - tmyklebu

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接