【问题标题】:why sometimes I get overflow and sometimes not in C为什么有时我会溢出,有时不会在 C 中
【发布时间】:2021-04-02 08:34:50
【问题描述】:

我有这段代码,两次计算给出了不同的结果。
第一个溢出,第二个只停留在 2^31-1。

我不知道为什么。

int n4, n5;

n4 = pow(2, 31);
n4 = n4 + n4;
n5 = pow(2, 31) + pow(2, 31);

printf("\nn4: %d, n5: %d",n4,n5);
/* n4: -2, n5: 2147483647 */

【问题讨论】:

  • 有符号整数算术溢出导致未定义行为。在大多数系统上,int 是 32 位宽(并且始终是有符号的)。在二进制补码系统上,它最多可以达到(2^31)-1
  • 在不相关的注释中,请养成在printf 调用中使用trailing 换行符的习惯。如果连接到终端或控制台窗口,则输出到stdout(其中printf rites)被缓冲,这意味着输出实际上是一个换行符。通过打印前导换行符,您可以编写 previous 行而不是当前行。最后输出和命令行提示符之间不会有换行符。
  • 看到下面 Allan Wind 的答案,我建议您关注到底是什么让您感到困惑。猜测一下,您确信做同样的事情(将 2 添加到自身的 31 的幂)并得到不同的结果。或者,当将其添加到自身时,您会发现结果为负 1 很奇怪。专注于您真正想知道的内容,减少有助于回答所需的猜测。

标签: c integer-overflow


【解决方案1】:

pow 的返回值是double 类型。

所以,pow(2, 31) + pow(2, 31)double 可以表示的范围内。

赋值n5 = (double) ...double 转换为int。但由于该值不能由 int 表示,因此您会得到 (2^31)-1 作为结果。基本上,您将double 值钳制为int ((2^31)-1) 的最大值。

第一种情况是溢出,因为n4 + n4 的结果是int 类型。并且值超过了最大值。

C 标准草案 N2176

6.3 次转化
6.3.1.4 实浮点数和整数

  1. 当实浮点类型的有限值转换为 _Bool 以外的整数类型时,小数部分将被丢弃(即,该值被截断为 0)。如果整数部分的值不能用整数类型表示,则行为未定义。

是的,转换是 UB。但为了解释 OP 的结果,我“假设”该值已被限制在 int 的范围内。

【讨论】:

  • 你已经得到了我的支持。为了收获更多,我建议您引用“基本上您将双精度值限制为 int ((2^31)-1) 的最大值”的来源。
  • 我认为“你钳”应该更像“gcc钳”。参见例如godbolt.org/z/aGK5hrqff
  • @Bob__ 有趣的是,如果您明确地转换为 int,例如:n5 = (int) (pow...),您不会收到任何警告。
  • gcc 有一堆溢出标志,如果你不知道的话。
  • @ErdalKüçük 是的,一个明确的演员表告诉编译器“闭嘴,我知道我在做什么”,即使情况并非如此......;)
【解决方案2】:

首先,“为什么有时会溢出而有时不会溢出”是完全不正确的。整数溢出是由于整数运算的结果无法以表达式的类型表示而导致的错误情况。您似乎对此感到困惑的是-2,它是环绕的一个示例,是整数溢出未定义行为的可能表现之一。问题是您无法从结果中判断是否发生了整数溢出,因为发生整数溢出后程序的行为是未定义的,因此结果看起来好像没有发生。


但是让我们仔细看看你的代码:

int n4, n5;

n4 = pow(2, 31);

pow(2, 31) 将导致一个值为2 的双精度提升到幂31 - 完全或近似,具体取决于实现的质量。确切值比 32 位 signed int 中可以存储的最大值多 1

通过截断小数 (C11/18 6.3.1.4p1)double 转换为 int

  1. 当实浮点类型的有限值转换为 _Bool 以外的整数类型时,小数部分将被丢弃(即,该值被截断为零)。如果整数部分的值不能用整数类型表示,则行为未定义

如果整数部分在目标类型中不可表示,则行为未定义。在这种情况下,如果 pow 实现不好,则该值可能略小于 2³¹,截断后的结果将等于 MAX_INT(即 2³¹-1)并且不会发生未定义的行为。但如果它是好的,那么它会导致值 2³¹ 并且转换的行为是不确定的。

现在我们开始:

n4 = n4 + n4;

如果 pow(2, 31); 转换为 int 导致未定义的行为,这已经是无稽之谈了。如果没有(即它导致 2³¹-1,那么这将导致整数溢出,因此这里会发生未定义的行为。

n5 = pow(2, 31) + pow(2, 31);

pow(2, 31) + pow(2, 31) 的值将是 double 类型的 2³²,并且无法在 32 位 signed int 中表示,因此转换将导致未定义的行为。

在这里,未定义的行为是未定义的:

printf("\nn4: %d, n5: %d", n4, n5);
/* n4: -2, n5: 2147483647 */

现在您认为究竟该结果是如何不确定的?嗯,它非常未定义:

如果你用编译

gcc pow.c

然后运行,你会得到

n4: -2, n5: 2147483647

如果你用gcc -fno-builtin-pow pow.c -lm编译,或者用scanf问指数31,你会得到

n4: 0, n5: -2147483648

未定义的行为是未定义的。

【讨论】:

    【解决方案3】:

    n4:第一个赋值溢出,因为 MAX_INT 可能是 2^31-1。根据 6.3.1.3,整数溢出是未定义的行为:

    否则,新类型是有符号的,值不能在其中表示;结果要么是实现定义的,要么引发实现定义的信号。

    在下一行中,添加之前得到的两个未定义值(即 -1)并得到 -2。

    n5:pow() 返回一个双精度数,因此右侧的表达式使用双精度数完成,然后转换为 int,这也是根据 6.3.1.4 的未定义行为:

    如果要转换的值在 可以表示但不能精确表示的值范围,结果是 以实现定义的方式选择最接近的较高或最接近较低的可表示值。

    【讨论】:

    • 你在后面的引文中引用了错误的段落
    • 其实两者都是错的。查看其他答案。
    • 实际上第一段中的另一个答案也是错误的:D真是一团糟。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-05-30
    • 1970-01-01
    • 1970-01-01
    • 2011-02-28
    • 1970-01-01
    • 2018-06-05
    相关资源
    最近更新 更多