为什么 long long 2147483647 + 1 = -2147483648?
why does 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 个字节,但为什么该代码会这样?
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++
中一样
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 的补码环绕只是 some/most 编译器做出的选择。
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++ 标准的情况下挑选任何东西(包括鼻恶魔)。在实践中,这种行为 (wrap + warn) 是不那么令人反感的行为之一,它源于将有符号整数溢出视为环绕,这在实践中经常发生 运行time.
此外,某些编译器可以选择实际 定义 所有情况下的正式行为,而不仅仅是编译时常量表达式。 (gcc -fwrapv
).
编译器会对此发出警告
好的编译器会在编译时显示许多形式的 UB 时发出警告,包括这个。 即使没有 -Wall
,GCC 和 clang 也会发出警告。来自 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 以来的 clang 开始默认启用此警告。
MSVC 仅警告 with -Wall
,这对于 MSVC 来说在大多数情况下都非常冗长,例如stdio.h
会产生大量警告,例如 'vfwprintf': unreferenced inline function has been removed
。 MSVC 对此的警告如下所示:
MSVC -Wall
<source>(5): warning C4307: '+': signed integral constant overflow
为什么这样设计:
To me, this question is asking, why doesn't the compiler also use the smallest data type that the result of a math operation will fit into? With integer literals, it would be possible to know at compile time that an overflow error was occurring. But the compiler does not bother to know this and handle it. Why is that?
“懒得处理”有点强;编译器确实会检测到溢出并发出警告。但它们遵循 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++ 源中的表达式,它的类型为 long
或 long long
。它被解析为 2147483648
,与一元运算符 -
分开,并且 2147483648
不适合 32 位有符号的 int
。因此它具有可以表示该值的下一个最大类型。
但是,任何受该扩展影响的程序在没有它的情况下都会有 UB(并且可能是回绕),并且扩展更有可能使代码恰好工作。这里有一个设计理念问题:太多的“碰巧工作”层和宽容的行为让人很难理解为什么某些东西 确实 工作,也很难证实它是否可以移植到具有其他类型宽度的其他实现。与 Java 这样的“安全”语言不同,C 非常不安全,并且在不同的平台上有不同的实现定义的东西,但许多开发人员只有一种实现可以测试。 (尤其是在互联网和在线持续集成测试之前。)
ISO C 没有定义行为,所以编译器 可以 将新行为定义为扩展,而不会破坏与任何无 UB 程序的兼容性。但是除非 every 编译器都支持它,否则您不能在可移植的 C 程序中使用它。我可以把它想象成至少 gcc/clang/ICC 支持的 GNU 扩展。
此外,这样的选项会与定义行为的 -fwrapv
有点冲突。总的来说,我认为它不太可能被采纳,因为有方便的语法来指定文字的类型(0x7fffffffUL + 1
给你一个 unsigned long
保证足够宽作为 32 位无符号的值整数。)
但是让我们首先将其视为 C 的选择,而不是当前的设计。
一种可能的设计是根据其值推断整个整数常量表达式的类型,以任意精度计算。为什么是任意精度而不是 long long
或 unsigned long long
?如果最终值由于 /
、>>
、-
或 &
运算符而较小,那么对于表达式的中间部分来说,这些值可能不够大。
或者像 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 位除法指令在商溢出时出错,而不是默默地 t运行cating 结果,所以即使将结果分配给 int
也不会'让编译器针对某些情况进行优化。)
此外,我们是否真的希望 a * b
的行为/定义取决于 a
和 b
是否为 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);
}
编译器确实会警告溢出 顺便说一句,通常您应该使用 -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;
^
另请注意,从技术上讲,int
和 long
及其变体取决于体系结构,因此它们的位长度可能会有所不同。
对于可预测大小的类型,使用 int64_t
、uint32_t
等通常在现代编译器和系统头文件中定义的类型会更好,因此无论您的应用程序是针对何种位数构建的,数据类型都保持可预测。另请注意,此类值的打印和扫描由 PRIu64
等宏复合而成。
为什么这段代码没有打印出相同的数字? :
long long a, b;
a = 2147483647 + 1;
b = 2147483648;
printf("%lld\n", a);
printf("%lld\n", b);
我知道int变量的最大数量是2147483647,因为int变量是4字节。 但据我所知,long long 变量是 8 个字节,但为什么该代码会这样?
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++
中一样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 的补码环绕只是 some/most 编译器做出的选择。
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++ 标准的情况下挑选任何东西(包括鼻恶魔)。在实践中,这种行为 (wrap + warn) 是不那么令人反感的行为之一,它源于将有符号整数溢出视为环绕,这在实践中经常发生 运行time.
此外,某些编译器可以选择实际 定义 所有情况下的正式行为,而不仅仅是编译时常量表达式。 (gcc -fwrapv
).
编译器会对此发出警告
好的编译器会在编译时显示许多形式的 UB 时发出警告,包括这个。 即使没有 -Wall
,GCC 和 clang 也会发出警告。来自 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 以来的 clang 开始默认启用此警告。
MSVC 仅警告 with -Wall
,这对于 MSVC 来说在大多数情况下都非常冗长,例如stdio.h
会产生大量警告,例如 'vfwprintf': unreferenced inline function has been removed
。 MSVC 对此的警告如下所示:
MSVC -Wall
<source>(5): warning C4307: '+': signed integral constant overflow
To me, this question is asking, why doesn't the compiler also use the smallest data type that the result of a math operation will fit into? With integer literals, it would be possible to know at compile time that an overflow error was occurring. But the compiler does not bother to know this and handle it. Why is that?
“懒得处理”有点强;编译器确实会检测到溢出并发出警告。但它们遵循 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++ 源中的表达式,它的类型为 long
或 long long
。它被解析为 2147483648
,与一元运算符 -
分开,并且 2147483648
不适合 32 位有符号的 int
。因此它具有可以表示该值的下一个最大类型。
但是,任何受该扩展影响的程序在没有它的情况下都会有 UB(并且可能是回绕),并且扩展更有可能使代码恰好工作。这里有一个设计理念问题:太多的“碰巧工作”层和宽容的行为让人很难理解为什么某些东西 确实 工作,也很难证实它是否可以移植到具有其他类型宽度的其他实现。与 Java 这样的“安全”语言不同,C 非常不安全,并且在不同的平台上有不同的实现定义的东西,但许多开发人员只有一种实现可以测试。 (尤其是在互联网和在线持续集成测试之前。)
ISO C 没有定义行为,所以编译器 可以 将新行为定义为扩展,而不会破坏与任何无 UB 程序的兼容性。但是除非 every 编译器都支持它,否则您不能在可移植的 C 程序中使用它。我可以把它想象成至少 gcc/clang/ICC 支持的 GNU 扩展。
此外,这样的选项会与定义行为的 -fwrapv
有点冲突。总的来说,我认为它不太可能被采纳,因为有方便的语法来指定文字的类型(0x7fffffffUL + 1
给你一个 unsigned long
保证足够宽作为 32 位无符号的值整数。)
但是让我们首先将其视为 C 的选择,而不是当前的设计。
一种可能的设计是根据其值推断整个整数常量表达式的类型,以任意精度计算。为什么是任意精度而不是 long long
或 unsigned long long
?如果最终值由于 /
、>>
、-
或 &
运算符而较小,那么对于表达式的中间部分来说,这些值可能不够大。
或者像 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 位除法指令在商溢出时出错,而不是默默地 t运行cating 结果,所以即使将结果分配给 int
也不会'让编译器针对某些情况进行优化。)
此外,我们是否真的希望 a * b
的行为/定义取决于 a
和 b
是否为 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);
}
编译器确实会警告溢出 顺便说一句,通常您应该使用 -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;
^
另请注意,从技术上讲,int
和 long
及其变体取决于体系结构,因此它们的位长度可能会有所不同。
对于可预测大小的类型,使用 int64_t
、uint32_t
等通常在现代编译器和系统头文件中定义的类型会更好,因此无论您的应用程序是针对何种位数构建的,数据类型都保持可预测。另请注意,此类值的打印和扫描由 PRIu64
等宏复合而成。