在C语言中将双精度浮点数转换为整数时如何处理溢出问题

34
今天,我注意到将一个大于最大整数的双精度浮点数转换为整数时,结果为-2147483648。同样,当我将一个小于最小整数的双精度浮点数转换为整数时,结果也为-2147483648。
这种行为在所有平台上都是定义好的吗?如何检测下溢/上溢?在类型转换之前放置min和max int的if语句是最佳解决方案吗?

将最小的32位整数(- 2147483648)强制转换为浮点数会得到正数(2147483648.0)。 - phuclv
9个回答

26

在将浮点数转换为整数时,溢出会导致未定义的行为。根据C99规范,第6.3.1.4节“实型和整型”:

当有限的实型值被转换为除_Bool以外的整型时,小数部分会被舍去(即值向零舍入)。如果整数部分的值无法由该整型表示,则行为是未定义的。

您需要手动检查范围,但不要使用以下代码

// DON'T use code like this!
if (my_double > INT_MAX || my_double < INT_MIN)
    printf("Overflow!");

INT_MAX是一个整数常量,可能没有精确的浮点表示。当与浮点数进行比较时,它可能会被四舍五入为最接近的高或低可表示浮点值(这是实现定义的)。例如,对于64位整数,INT_MAX2^63-1,通常会被舍入为2^63,因此检查实际上变成了my_double > INT_MAX + 1。如果my_double等于2^63,则不会检测到溢出。

例如,在Linux上使用gcc 4.9.1的以下程序:

#include <math.h>
#include <stdint.h>
#include <stdio.h>

int main() {
    double  d = pow(2, 63);
    int64_t i = INT64_MAX;
    printf("%f > %lld is %s\n", d, i, d > i ? "true" : "false");
    return 0;
}

打印

9223372036854775808.000000 > 9223372036854775807 is false

如果您不事先了解整数和双精度类型的限制和内部表示,那么很难做到正确。但是,例如,如果您从 double 转换为 int64_t ,则可以使用完全等于浮点常数的精确双精度浮点数(假设使用二进制补码和IEEE浮点数):

if (!(my_double >= -9223372036854775808.0   // -2^63
   && my_double <   9223372036854775808.0)  // 2^63
) {
    // Handle overflow.
}

构造函数!(A && B)也可以正确处理NaN。对于int,有一个便携式、安全但略微不准确的版本:
if (!(my_double > INT_MIN && my_double < INT_MAX)) {
    // Handle overflow.
}

这种方法比较谨慎,会错误地拒绝等于INT_MININT_MAX的值。但对于大多数应用程序来说,这应该是可以接受的。


我刚刚进行了一些实证测试,这个答案似乎是正确的(再次假设是二进制补码整数;如果不能假设,也许Boost或SafeInt是唯一合理的方法)。您应该点赞这个答案,并对那个主张my_double>INT_MAX || my_double<INT_MIN的不正确答案进行反对;事实上是不正确的。 - bhaller
1
@bhaller,我刚刚检查了一下,Boost和SafeInt都犯了我在回答中讨论的同样的错误。 - nwellnhof
哎呀,你向他们报告了这个问题吗? - bhaller
2
@bhaller 是的,这里这里 - nwellnhof

14

limits.h包含整数数据类型的最大和最小可能值的常量,您可以在进行强制类型转换之前检查双精度变量,例如:

if (my_double > nextafter(INT_MAX, 0) || my_double < nextafter(INT_MIN, 0))
    printf("Overflow!");
else
    my_int = (int)my_double;

编辑: nextafter() 将解决nwellnhof提到的问题


这对于浮点数在我这里不起作用。我尝试这样做 float f = INT_MAX; f++; ConvertToInt(f),使用您上面的限制检查,但它不会溢出。 有什么不同吗? - Pittfall
1
@Pittfall: float(除非是非常奇特的平台,否则都使用IEEE-754浮点数)有24个有效二进制数字。因此,当您将其设置为INT_MAX时,即2³¹-1(32位平台上的INT_MAX),最后一位是128。因此,如果您添加的任何内容小于128,则结果是原始数字,即(float)INT_MAX + 1.f == (float)INT_MAX。对于具有比int更多有效数字的double,它将起作用。 - Jan Hudec
INT_MAXINT_MIN是C语言检查的方式。C++的方式是使用std::numeric_limits<int>::max()...::min() - Jan Hudec
10
这个答案是错误的,因为INT_MININT_MAX没有精确的浮点数表示是无法保证的。例如,在使用64位整数时,INT_MAX是2^63-1,而(double)INT_MAX会四舍五入为2^63,所以如果my_double为2 ^ 63,则此检查将无法检测到溢出。将检查改为my_double >= INT_MAX || my_double < INT_MIN应该可以在使用二进制补码整数时正常工作,即使看起来是错误的。 - nwellnhof
1
我同意这个答案是错误的,它应该被删除。 - bhaller
3
nextafter 的唯一问题是,如果一个等于 INT_MINdouble 被转换成 int,那么它将被检测为溢出。除此之外,它是一个不错的、可移植的解决方案。 - nwellnhof

13
回答您的问题:当您将超出范围的浮点数强制转换时,其行为是未定义的或实现特定的。
从个人经验来看:我曾在一个MIPS64系统上工作,该系统根本没有实现这些类型的强制转换。CPU不执行确定性操作,而是抛出了一个CPU异常。本应模拟强制转换的异常处理程序返回结果前未对结果进行任何操作。
最终我得到了随机整数。请猜测一下追踪错误到这个原因花费了多长时间 :-) 如果您不能确定数字是否会超出有效范围,最好自己进行范围检查。

4
什么是检测下溢/上溢的最佳方法?
将截断的double与INT_MIN,INT_MAX附近的精确极限进行比较。
诀窍在于将基于INT_MIN,INT_MAX的极限精确地转换为double值。double可能无法准确表示INT_MAX,因为int中的位数可能超过浮点数的精度*1。在这种情况下,将INT_MAX转换为double会受到舍入的影响。 INT_MAX之后的数字是2的幂,并且肯定可以表示为double。 2.0 *(INT_MAX / 2 + 1)生成整数,其值比INT_MAX大1。
非2s补码机器上也适用于INT_MIN。
INT_MAX始终是2的幂-1。 INT_MIN始终是: -INT_MAX(不是2的补码)或 -INT_MAX-1(2的补码)。
int double_to_int(double x) {
  x = trunc(x);
  if (x >= 2.0*(INT_MAX/2 + 1)) Handle_Overflow();
  #if -INT_MAX == INT_MIN
  if (x <= 2.0*(INT_MIN/2 - 1)) Handle_Underflow();
  #else

  // Fixed 2022
  // if (x < INT_MIN) Handle_Underflow();
  if (x - INT_MIN < -1.0) Handle_Underflow();

  #endif
  return (int) x;
}

检测 NaN 并避免使用 trunc()
#define DBL_INT_MAXP1 (2.0*(INT_MAX/2+1)) 
#define DBL_INT_MINM1 (2.0*(INT_MIN/2-1)) 

int double_to_int(double x) {
  if (x < DBL_INT_MAXP1) {
    #if -INT_MAX == INT_MIN
    if (x > DBL_INT_MINM1) {
      return (int) x;
    }
    #else
    if (ceil(x) >= INT_MIN) {
      return (int) x;
    }
    #endif 
    Handle_Underflow();
  } else if (x > 0) {
    Handle_Overflow();
  } else {
    Handle_NaN();
  }
}

[编辑2022] 6年后修正了角落错误。
范围在`(INT_MIN - 1.0 ... INT_MIN)`(不包括端点)内的double值可以很好地转换为int。以前的代码无法处理这些值。 *1int精度大于double时,INT_MIN - 1也适用于此。虽然这很少见,但问题也很容易适用于long long。请考虑以下两者之间的区别:
  if (x < LLONG_MIN - 1.0) Handle_Underflow(); // Bad
  if (x - LLONG_MIN < -1.0) Handle_Underflow();// Good

使用二进制补码,some_int_type_MIN 是一个(负的)2次幂,并且可以精确地转换为double。因此,在关注范围内,x - LLONG_MIN 是准确的,而LLONG_MIN - 1.0 在减法中可能会失去精度。


你说一个2的幂次方,即MAX_INT+1的大小,肯定可以用double表示。你能解释一下为什么吗?你的假设是什么?假设IEEE足够吗? - Kristian Spangsege
1
好的,我现在可以看到你假设FLT_RADIX是2,并且INT_MAX远低于DBL_MAX,除了语言标准所保证的内容之外没有其他内容。这是一个很棒和有创意的解决方案。我喜欢它。 - Kristian Spangsege
一个评论:你所说的下溢,我认为正式称为负溢出。当结果被截断为零时,才是下溢。溢出是指影响幅度的情况,粗略地说。 - Kristian Spangsege
1
在C语言中,FP的下溢与注释匹配,而C使用上溢来表示超出 int范围的结果(+和 - )。 在口语中,我听过和读过“下溢”用于过度负面的结果,由于OP也是这样使用它,因此回答同样有意义。“负数溢出”是一个好术语,即使有点啰嗦。 - chux - Reinstate Monica
严格来说,还有一个假设,即 DBL_MANT_DIG <= DBL_MAX_EXP,这样 ceil() 的结果总是可表示的。IEEE 类型满足此条件。我从 Linux 的 ceil() 手册中了解到这一点。 - Kristian Spangsege

4

一个适用于C++的便携式方法是使用SafeInt类:

http://www.codeplex.com/SafeInt

该实现允许对C++数字类型进行常规的加法/减法等运算,包括强制转换。当检测到溢出情况时,它将抛出异常。

SafeInt<int> s1 = INT_MAX;
SafeInt<int> s2 = 42;
SafeInt<int> s3 = s1 + s2;  // throws

我强烈建议在任何需要考虑溢出的场景中使用这个类。它可以很好地避免悄无声息地溢出。如果发生溢出,只需捕获SafeIntException并根据情况进行恢复即可。

现在,SafeInt已经可以在GCC和Visual Studio上使用了。


一个初步测试让我相信,在C ++中可能无法检测溢出(除非严重的开销或完全改变范例,例如将每个整数都包装为对象)。这个“专用类”似乎不能处理SafeInt<size_t> x = std :: numeric_limits<size_t>::max() + 100(它不会抛出异常)。 - kizzx2
2
你在开玩笑吧...?你的右侧在到达SafeInt构造函数之前就已经溢出了。你不能责怪SafeInt。 - Ichthyo
1
SafeInt在我讨论的答案中犯了同样的错误。SafeInt<int64_t> v = pow(2.0, 63.0)不会抛出异常。 - nwellnhof

2
另一个选项是使用boost::numeric_cast,它允许在数字类型之间进行任意转换。当数字类型被转换时,它会检测到范围的损失,并且如果无法保留范围,则会抛出异常。
上面引用的网站还提供了一个小例子,可以快速了解如何使用这个模板。
当然,这不再是纯C了;-)

boost::numeric_cast在我回答中讨论的问题上犯了同样的错误。numeric_cast<int64_t>(pow(2.0, 63.0))不会抛出异常。 - nwellnhof

2
我们遇到了同样的问题。例如:
double d = 9223372036854775807L;
int i = (int)d;

在Linux/Windows中,i的值为-2147483648。但在AIX 5.3中,i的值为2147483647。
如果double超出了整数范围。
- Linux/Windows总是返回INT_MIN。 - 如果double为正数,则AIX将返回INT_MAX;如果double为负数,则返回INT_MIN。

1
Linux 绝对不会总是返回 INT_MIN,如果 Windows 真的这样做了,那我会很惊讶,因为这是处理器架构本身的功能(它不是操作系统特定的)。 - al45tair
printf("%d\n", (int) (double) (9223372036854775807L));会打印出2147483647,但是double d = 9223372036854775807L; int i = (int) d; printf("%d", i);会打印出-2147483648。在Windows上使用MinGW730_64。 - Xixiaxixi

0

我不确定,但我认为可能可以为下溢/上溢“打开”浮点异常...看看这个在MSVC7\8中处理浮点异常,所以你可能有一个替代if/else检查的选择。


-1

我不能确定它是否适用于所有平台,但在我使用的每个平台上,基本上就是这样发生的。除了在我的经验中,它会滚动。也就是说,如果double的值为INT_MAX + 2,则当强制转换的结果最终成为INT_MIN + 2时。

至于最佳处理方法,我真的不确定。我自己也遇到了这个问题,但尚未找到一种优雅的处理方式。我相信会有人回复并帮助我们两个。


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