【问题标题】:why does long long 2147483647 + 1 = -2147483648? [duplicate]为什么 long long 2147483647 + 1 = -2147483648? [复制]
【发布时间】:2020-08-20 19:19:49
【问题描述】:

为什么这段代码不打印相同的数字? :

long long a, b;
a = 2147483647 + 1;
b = 2147483648;
printf("%lld\n", a);
printf("%lld\n", b);

我知道 int 变量的最大数量是 2147483647,因为 int 变量是 4 字节。 但据我所知,long long 变量是 8 字节,但为什么那个代码会这样呢?

【问题讨论】:

  • @KerryCao sizeof(int) 取决于硬件,但在现代硬件上通常是 4 个字节。 stackoverflow.com/questions/11438794/…
  • @Hoseong,你看到一些编译警告了吗?
  • 请注意,签名溢出在 C++ 中过去和现在都是未定义行为。你的编译器应该警告你这个问题。 godbolt.org/z/krZBUa
  • 要获取类型的实际范围,#include <limits> 并查看std::numeric_limits<int>::min()std::numeric_limits<int>::max()。将int 替换为您真正感兴趣的整数类型。(您也可以将这两个函数用于浮点类型,但std::numeric_limits<double>::min() 的定义有些不直观。)

标签: c++ c undefined-behavior integer-overflow twos-complement


【解决方案1】:

2147483647 + 1 被评估为两个ints 的总和,因此会溢出。

2147483648 太大而无法放入int,因此编译器假定为long(或MSVC 中的long long)。因此它不会溢出。

要以long long 的形式执行求和,请使用适当的常量后缀,即

a = 2147483647LL + 1;

【讨论】:

【解决方案2】:

这种有符号整数溢出是未定义的行为,就像在 C/C++ 中一样

What Every C Programmer Should Know About Undefined Behavior

除非您使用 gcc -fwrapv 或等效项进行编译以使有符号整数溢出明确定义为 2 的补码环绕。对于 gcc -fwrapv 或任何其他定义整数溢出 = 环绕的实现,您在实践中碰巧看到的环绕是明确定义的,并且遵循其他 ISO C 规则,用于整数文字类型和评估表达式。

T var = expression 仅将表达式隐式转换为类型T 根据标准规则评估表达式。喜欢(T)(expression),不像(int64_t)2147483647 + (int64_t)1

编译器可能会选择假设永远不会到达此执行路径并发出非法指令或其他东西。在常量表达式的溢出中实现 2 的补码环绕只是一些/大多数编译器做出的选择。


ISO C 标准规定数字文字的类型为 int,除非值太大而无法容纳(可以是 long or long long, or unsigned for hex),或者如果使用了大小覆盖。然后,通常的整数提升规则适用于像 +* 这样的二元运算符,无论它是否是编译时常量表达式的一部分。

这是一个简单且一致的规则,编译器很容易实现,即使在 C 的早期,编译器必须在有限的机器上运行。

因此,在 ISO C/C++ 中,2147483647 + 1 在 32 位 int 的实现中是未定义的行为将其视为int(并因此将值包装为有符号负数)自然遵循表达式应具有何种类型的 ISO C 规则,以及非溢出情况的正常评估规则。当前的编译器不会选择以不同的方式定义行为。

ISO C/C++ 确实未定义它,因此实现可以在不违反 C/C++ 标准的情况下从字面上挑选任何东西(包括鼻恶魔)。在实践中,这种行为(换行 + 警告)是不太令人反感的行为之一,并将有符号整数溢出视为换行,这在实际运行时经常发生。

此外,一些编译器可以选择在所有情况下正式定义该行为,而不仅仅是编译时常量表达式。 (gcc -fwrapv)。


编译器会对此发出警告

好的编译器会在编译时出现多种形式的 UB 时发出警告,包括这种形式。 GCC 和 clang 即使没有 -Wall 也会发出警告。来自the Godbolt compiler explorer

  clang
<source>:5:20: warning: overflow in expression; result is -2147483648 with type 'int' [-Winteger-overflow]
    a = 2147483647 + 1;
                   ^
  gcc
<source>: In function 'void foo()':
<source>:5:20: warning: integer overflow in expression of type 'int' results in '-2147483648' [-Woverflow]
    5 |     a = 2147483647 + 1;
      |         ~~~~~~~~~~~^~~

GCC 至少从 2006 年的 GCC4.1(Godbolt 上的最旧版本)开始默认启用此警告,并从 3.3 开始发出叮当声。

MSVC 只警告 with -Wall,这对于 MSVC 来说大部分时间都是不可用的冗长,例如stdio.h 会产生大量警告,例如 'vfwprintf': unreferenced inline function has been removed。 MSVC 对此的警告如下:

  MSVC -Wall
<source>(5): warning C4307: '+': signed integral constant overflow

@HumanJHawkins asked为什么会这样设计:

对我来说,这个问题是在问,为什么编译器不使用数学运算结果适合的最小数据类型?使用整数文字,可以在编译时知道发生了溢出错误。但是编译器不会费心去了解和处理它。这是为什么呢?

“懒得处理”有点强;编译器确实会检测到溢出并发出警告。但它们遵循 ISO C 规则,即 int + int 的类型为 int,并且每个数字文字的类型为 int。编译器只是故意选择包装而不是加宽并赋予表达式不同于您期望的类型。 (而不是完全因为 UB 来救助。)

在运行时发生带符号溢出时,包装很常见,尽管在循环中编译器会积极优化 int i / array[i]avoid redoing sign-extension every iteration

由于与格式字符串的类型不匹配,扩展会带来自己的(较小的)陷阱集,例如 printf("%d %d\n", 2147483647 + 1, 2147483647); 具有未定义的行为(并且在 32 位机器上实际上失败)。如果2147483647 + 1 隐式提升为long long,则需要%lld 格式字符串。 (实际上它会中断,因为 64 位 int 通常在 32 位机器上的两个 arg 传递槽中传递,所以第二个 %d 可能会看到第一个 long long 的第二半。)

公平地说,这对-2147483648 来说已经是个问题了。作为 C/C++ 源代码中的表达式,它的类型为 longlong long。它与一元 - 运算符分开解析为 2147483648,并且 2147483648 不适合 32 位有符号 int。因此,它具有可以表示该值的下一个最大类型。

但是,任何受该扩展影响的程序在没有它的情况下都会有 UB(并且可能是包装),并且扩展更有可能使代码发生工作。这里有一个设计理念问题:太多的“碰巧工作”和宽容的行为使得很难理解为什么某件事确实工作,并且很难证实它是否可以移植到其他实现中其他类型的宽度。与 Java 等“安全”语言不同,C 非常不安全,并且在不同的平台上具有不同的实现定义的东西,但许多开发人员只有一种实现可以测试。 (尤其是在互联网和在线持续集成测试之前。)


ISO C 没有定义行为,所以是的,编译器可以将新行为定义为扩展,而不会破坏与任何无 UB 程序的兼容性。但是除非每个编译器都支持它,否则你不能在可移植的 C 程序中使用它。我可以把它想象成至少 gcc/clang/ICC 支持的 GNU 扩展。

此外,这样的选项会与确实定义行为的-fwrapv 发生冲突。总的来说,我认为它不太可能被采用,因为有一种方便的语法来指定文字的类型(0x7fffffffUL + 1 为您提供了一个 unsigned long,它保证对于该值作为 32 位无符号整数来说足够宽。)

但让我们首先将其视为 C 的选择,而不是当前的设计。

一种可能的设计是从其值推断整个整数常量表达式的类型,以任意精度计算。为什么使用任意精度而不是long longunsigned long long?如果由于 /&gt;&gt;-&amp; 运算符而导致最终值很小,那么对于表达式的中间部分来说,这些可能不够大。

或者更简单的设计,例如 C 预处理器,其中常量整数表达式以某个固定的实现定义的宽度(例如至少 64 位)进行评估。 (然后根据最终值或表达式中最宽的临时值分配类型?)但这对于 16 位机器上的早期 C 有明显的缺点,它使编译时表达式的计算速度比 if 慢编译器可以在内部为int 表达式使用机器的本机整数宽度。

整数常量表达式在 C 中已经有些特殊,在某些情况下需要在编译时进行评估,例如对于static int array[1024 * 1024 * 1024];(其中乘法将在具有 16 位整数的实现上溢出。)

显然我们不能有效地将提升规则扩展到非常量表达式;如果 (a*b)/c 在 32 位机器上可能必须将 a*b 评估为 long long 而不是 int,则除法将需要扩展精度。 (例如,x86 的 64 位 / 32 位 => 32 位除法指令在商溢出时出错,而不是静默截断结果,因此即使将结果分配给 int 也不会让编译器针对一些情况。)

另外,我们是否真的希望a * b 的行为/定义取决于ab 是否为static const 让编译时评估规则与规则匹配因为非常量表达式通常看起来不错,即使它留下了这些讨厌的陷阱。但同样,好的编译器可以在常量表达式中警告这一点。


这个 C 陷阱的其他更常见的情况是使用 1&lt;&lt;40 而不是 1ULL &lt;&lt; 40 来定义位标志,或者将 1T 写为 1024*1024*1024*1024

【讨论】:

  • 嗯,我想知道,由于未定义有符号溢出,编译器是否可以决定将结果视为long?或者也许这就是您从值推断类型的意思,但确实提到“遵循 ISO C 规则,说表达式具有 int 类型”,所以我完全确定是否允许这样做。
  • @ilkkachu:我的回答确实有点自相矛盾>.2147483647 + 1 可以评估为 int64_t。但编译器实际做的是应用 ISO C 类型规则,然后在编译时评估期间包装整数值。 ISO C 在溢出情况下不需要(或其他任何东西)。欢迎提出措辞建议或修改。
  • @ilkkachu:更新了更好的措辞。说“ISO C 要求它是 UB”暗示了关于 UB 是什么的各种错误想法。例如它需要崩溃或需要警告,或者其他什么。实际上,这只是意味着您处于未知领域,ISO C 标准对程序没有任何意义。您选择做的任何事情要么只是碰巧工作,要么是实现定义的行为。 (在标准未定义行为的情况下,实现 100% 允许定义他们想要的任何行为,例如 gcc -fwrapv-fno-strict-aliasing。)
  • @ilkkachu:嗯,对,我之前写的那部分内容我没有看那么多。 ISO C 规则确实说int * intint。当前的编译器始终遵循该规则。因此,任何让它以不同方式工作的提议都会覆盖这种情况下的规则,这是对当前编译器的任何提议扩展的缺点之一。甚至从历史语言设计的角度来看,C 最初是如何设计的。如果 printf("%d %d\n", 2147483647 + 1, 2147483647); 由于与 fmt 字符串的类型不匹配而失败,这不是一件好事。
  • @dmeister:在许多平台和情况下,以算术正确的方式执行某些计算可能比使用环绕语义(例如(x*12345)/12345)更便宜,所以我不认为这是惊人的优化这可能会导致计算表现得好像使用未指定的较大类型执行。更令人惊讶的是,在 gcc 中,unsigned mul_mod_65536(unsigned short x, unsigned short y) { return (x*y) &amp; 0xFFFFu;} 只有在 x 不超过 2147483647/y. 时才能可靠地工作。
【解决方案3】:

好问题。正如其他人所说,默认情况下数字为int,因此您对a 的操作作用于两个ints 并溢出。我试图重现这一点,并扩展一点以将数字转换为long long 变量,然后将1 添加到其中,如下面的c 示例:

$ cat test.c 
#include <stdlib.h>
#include <stdint.h>
#include <stdio.h>

void main() {
  long long a, b, c;

  a = 2147483647 + 1;
  b = 2147483648;

  c = 2147483647;
  c = c + 1;

  printf("%lld\n", a);
  printf("%lld\n", b);
  printf("%lld\n", c);
}

编译器确实会警告溢出顺便说一句,通常你应该使用-Werror -Wall 编译生产代码以避免这样的事故:

$ gcc -m64 test.c -o test
test.c: In function 'main':
test.c:8:16: warning: integer overflow in expression [-Woverflow]
 a = 2147483647 + 1;
                ^

最后,测试结果如预期(int溢出第一种情况,long long int第二种和第三种情况):

$ ./test 
-2147483648
2147483648
2147483648

另一个 gcc 版本进一步警告:

test.c: In function ‘main’:
test.c:8:16: warning: integer overflow in expression [-Woverflow]
 a = 2147483647 + 1;
                ^
test.c:9:1: warning: this decimal constant is unsigned only in ISO C90
 b = 2147483648;
 ^

另请注意,从技术上讲,intlong 及其变体取决于架构,因此它们的位长可能会有所不同。 对于可预测大小的类型,您最好使用现代编译器和系统头文件中通常定义的int64_tuint32_t 等,因此无论您的应用程序构建的位数如何,数据类型仍然是可预测的。另请注意,此类值的打印和扫描由 PRIu64 等宏组合而成。

【讨论】:

  • 我尝试构建-m32-m16 版本。这在我的 Debian VM 上失败了,因为它缺少适当的头文件来完成编译。在 OpenIndiana (OpenSolaris-next-of-kin) 上,32 位版本的构建和工作方式与 64 位版本相同,而 16 位版本在main() 中出现了段错误,因此结果尚无定论:) 我希望说明它打印不同的整数值,因为intlong long 定义在这些位数中可能会有所不同。
  • x86 gcc -m16 生成在 16 位模式下运行但仍使用 32 位操作数大小的代码(并且 ABI 与 32 位相同,例如 int 仍然是 int32_t) .我不认为你可以在 GNU/Linux 下运行生成的二进制文件,即使你有库。 gcc -m64gcc -m32 对于 intlong long 的大小也相同,只是在 long 的宽度上存在分歧。有时 ISA 细节对 UB 很重要,但在这种情况下不重要。该行为在整数常量表达式求值期间有效地由实现定义,在运行时不会发生任何奇怪的事情。
  • 顺便说一句,您可以通过installing gcc-multilibgcc -m32 在Debian 上工作
【解决方案4】:

因为 C/C++ 中的 int 范围是 -2147483648+2147483647

所以当你添加1 时,它会超出int 的最大限制。

为了更好地理解,假设int 的整个范围以正确的顺序放在一个圆圈上:

2147483647 + 1 == -2147483648

2147483647 + 2 == -2147483647

如果您想克服这个问题,请尝试使用long long 而不是int

【讨论】:

  • 这只能保证与gcc -fwrapv一起定义签名溢出的行为。
  • 这是对 32 位 2 的补码符号环绕的准确描述,这实际上是当前 C 编译器所做的。但是说 C/C++ 中 int 的范围是 ... 是不准确的,没有任何限定符。具有 16 位 int 的微控制器和 DSP 的 C 实现仍然很普遍,而 C 只要求范围至少 -32767 .. 32767,并且它是 2 的补码、1 的补码或符号/幅度. en.cppreference.com/w/cpp/language/types
猜你喜欢
  • 2016-04-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-05-07
  • 1970-01-01
  • 2017-04-12
相关资源
最近更新 更多