为什么 long long 2147483647 + 1 = -2147483648?

为什么这个代码不打印相同的号码:

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

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

10321 次浏览

2147483647 + 1计算为两个 ints之和,因此溢出。

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

要将求和作为 long long执行,请使用适当的常量后缀,即。

a = 2147483647LL + 1;

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

因此,当您添加 1时,它会溢出 int的最大限制。

为了更好地理解,假设 int的整个范围按照适当的顺序放在一个圆上:

2147483647 + 1 == -2147483648


2147483647 + 2 == -2147483647

如果你想克服这一点,尝试使用 long long而不是 int

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

每个 c 程序员都应该知道的未定义行为

除非您使用 gcc -fwrapv编译,或者等效于将有符号整数溢出定义为2的补语环绕。对于定义了整数溢出 = 包装的 gcc -fwrapv或任何其他实现,您在实践中碰巧看到的包装是定义良好的,并遵循其他 ISO C 规则来定义整数文字类型和计算表达式。

T var = expression仅隐式地将表达式转换为根据标准规则计算表达式的 T之后类型。像 (T)(expression)不像 (int64_t)2147483647 + (int64_t)1

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


ISO C 标准指定了 数值文字的类型为 int,除非该值太大而不适合(它可以是 长的或长的长的,或没有签名的六角形) ,或者如果使用了大小覆盖。然后,通常的整数提升规则适用于像 +*这样的二进制运算符,不管它是否是编译时常量表达式的一部分。

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

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

ISO C/C + + 确实没有定义它,所以实现可以挑选任何东西(包括鼻腔恶魔)而不违反 C/C + + 标准。在实践中,这种行为(包装 + 警告)是不那么令人讨厌的行为之一,它是将有符号整数溢出视为包装的结果,这是在运行时实践中经常发生的情况。

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


编译器会对此发出警告

优秀的编译器在编译时可见很多形式的 UB 时会发出警告,包括以下内容。即使没有 -Wall,海湾合作委员会和叮当警告。来自 Godbolt 编译器浏览器:

  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;
|         ~~~~~~~~~~~^~~

自从2006年 GCC4.1(Godbolt 上最老的版本)以来,GCC 至少在默认情况下启用了这个警告,并且自3.3以来一直叮当作响。

MSVC 只警告 -Wall,对于 MSVC 来说,-Wall大多数时候冗长得无法使用,例如,stdio.h会导致大量类似于 'vfwprintf': unreferenced inline function has been removed的警告。MSVC 对此的警告似乎是:

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

@ HumanJHawkins 问为什么设计成这样:

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

“不用麻烦去处理它”有点强烈; 编译器确实会检测到溢出并对其发出警告。但是它们遵循 ISO C 规则,即 int + int的类型为 int,数值文字的类型为 int。编译器只是故意选择包装,而不是扩大表达式并给出与预期不同的类型。(而不是完全因为 UB 而纾困。)

当有符号溢出发生在运行时时,包装是常见的,尽管 in 循环编译器会积极地优化 int i/array[i]避免每次迭代重做符号扩展

由于与格式字符串的类型不匹配,扩展会带来自己的(较小的)陷阱,比如 printf("%d %d\n", 2147483647 + 1, 2147483647);具有未定义的行为(在32位机器上实际上会失败)。如果 2147483647 + 1隐式提升为 long long,则需要一个 %lld格式的字符串。(实际上它会中断,因为64位的 int 通常在32位机器上的两个参数传递槽中传递,所以第二个 %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?如果由于 />>-&操作符的原因,最终值很小,那么这些值对于表达式的中间部分来说可能不够大。

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

整数常量表达式在 C 中已经有些特殊了,在某些情况下需要在编译时求值 ,例如对于 static int array[1024 * 1024 * 1024];(在16位 int 的实现中,乘数会溢出)

显然,我们不能有效地将提升规则扩展到非常数表达式; 如果在32位机器上,(a*b)/c可能必须将 a*b计算为 long long而不是 int,那么除法将需要更高的精度。(例如 x86的64位/32位 = > 32位除法指令在商溢出时出错,而不是静默地截断结果,所以即使将结果分配给 int,在某些情况下也不会让编译器优化得很好)

此外,我们真的希望 a * b的行为/定义取决于 ab是否是 static const?使编译时计算规则与非常量表达式的规则相匹配通常看起来不错,尽管它留下了这些令人讨厌的陷阱。但是,这也是优秀的编译器可以在常量表达式中警告的地方。


这种 C 陷阱的其他更常见的情况是像 1<<40而不是 1ULL << 40来定义一个位标志,或者将1T 写成 1024*1024*1024*1024

问得好。正如其他人所说的,默认情况下数字是 int,因此对 a的操作作用于两个 int和溢出。我尝试重现这一点,并扩展一点,将数字转换为 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);
}

编译器确实会对溢出 BTW 发出警告,通常您应该使用 -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

另一个海湾合作委员会的版本进一步警告:

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这样的宏复合而成的。