Hos*_*eon 75 c c++ integer-overflow undefined-behavior twos-complement
为什么此代码不打印相同的数字?:
long long a, b;
a = 2147483647 + 1;
b = 2147483648;
printf("%lld\n", a);
printf("%lld\n", b);
Run Code Online (Sandbox Code Playgroud)
我知道 int 变量的最大数量是 2147483647,因为 int 变量是 4 字节。但据我所知,long long 变量是 8 字节,但为什么这段代码会这样呢?
Pau*_*ers 128
2147483647 + 1被评估为两个的总和,ints因此溢出。
2147483648太大而无法放入 an 中int,因此编译器将其假定为 a long(或long longMSVC 中的 a)。因此它不会溢出。
要执行求和作为long long使用适当的常数后缀,即
a = 2147483647LL + 1;
Run Code Online (Sandbox Code Playgroud)
Pet*_*des 16
除非您使用gcc -fwrapv或等效的方式进行编译以使有符号整数溢出明确定义为 2 的补码环绕。使用gcc -fwrapv或任何其他定义整数溢出 = 环绕的实现,您在实践中碰巧看到的环绕是明确定义的,并且遵循其他 ISO C 规则,用于整数文字和计算表达式的类型。
T var = expression仅在根据标准规则对表达式求值T 后隐式地将表达式转换为类型。喜欢(T)(expression),不喜欢(int64_t)2147483647 + (int64_t)1。
编译器可以选择假设这条执行路径永远不会到达并发出非法指令或其他东西。在常量表达式中对溢出实现 2 的补码环绕只是某些/大多数编译器所做的选择。
ISO C 标准指定数字文字具有类型,int除非值太大而无法容纳(它可以是long 或 long long,或者 hex 无符号),或者如果使用了大小覆盖。然后通常的整数提升规则适用于+和这样的二元运算符*,无论它是否是编译时常量表达式的一部分。
这是一个简单且一致的规则,编译器很容易实现,即使在 C 的早期,编译器必须在有限的机器上运行。
因此,在ISO C / C ++2147483647 + 1是未定义行为上与32位实施方案int。 将其视为int(从而将值包装为带符号的负数)自然遵循 ISO C 规则关于表达式应该具有的类型,以及非溢出情况的正常评估规则。当前的编译器不会选择以不同的方式定义行为。
ISO C/C++ 确实没有定义它,因此实现可以在不违反 C/C++ 标准的情况下选择任何东西(包括鼻妖)。在实践中,这种行为 (wrap + warn) 是不太令人反感的行为之一,并且遵循将有符号整数溢出视为包装,这是在运行时在实践中经常发生的情况。
此外,一些编译器可以选择为所有情况正式定义该行为,而不仅仅是编译时常量表达式。( gcc -fwrapv).
当 UB 在编译时可见时,好的编译器会警告许多形式的 UB,包括这个。 即使没有-Wall. 来自Godbolt 编译器资源管理器:
clang
<source>:5:20: warning: overflow in expression; result is -2147483648 with type 'int' [-Winteger-overflow]
a = 2147483647 + 1;
^
Run Code Online (Sandbox Code Playgroud)
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;
| ~~~~~~~~~~~^~~
Run Code Online (Sandbox Code Playgroud)
至少从 2006 年的 GCC4.1(Godbolt 上的最旧版本)和 3.3 开始,GCC 默认启用了这个警告。
MSVC只警告用 -Wall,这对于MSVC是unusably冗长的大部分时间,例如stdio.h导致吨喜欢的警告'vfwprintf': unreferenced inline function has been removed。MSVC 对此的警告如下:
MSVC -Wall
<source>(5): warning C4307: '+': signed integral constant overflow
Run Code Online (Sandbox Code Playgroud)
@HumanJHawkins 问为什么这样设计:
对我来说,这个问题是问,为什么编译器不也使用数学运算结果适合的最小数据类型?使用整数文字,可以在编译时知道发生了溢出错误。但是编译器并不费心去了解和处理它。这是为什么?
“懒得处理”有点强;编译器确实检测到溢出并发出警告。但它们遵循 ISO C 规则,即int + inthas type int,并且每个数字文字都有 type int。编译器只是故意选择换行而不是加宽并为表达式提供与您预期不同的类型。(而不是完全因为 UB 而纾困。)
在运行时发生有符号溢出时,包装很常见,尽管在循环中编译器会积极优化int i/array[i]以避免每次迭代重做符号扩展。
printf("%d %d\n", 2147483647 + 1, 2147483647);由于与格式字符串的类型不匹配,加宽会带来自己的(较小的)陷阱,例如具有未定义的行为(并且在 32 位机器上实际上失败)。如果2147483647 + 1隐式提升为long long,则需要一个%lld格式字符串。(并且它在实践中会中断,因为 64 位 int 通常在 32 位机器上的两个 arg-passing 插槽中传递,因此第二个%d可能会看到第一个的第二半long long。)
公平地说,这已经是-2147483648. 作为 C/C++ 源代码中的表达式,它具有类型longor long long。它2147483648与一元运算-符分开解析,并且2147483648不适合 32 位 signed int。因此,它具有可以表示值的下一个最大类型。
但是,任何受该扩展影响的程序都会有 UB(并且可能会包装),而没有它,扩展更有可能使代码能够正常工作。这里有一个设计理念问题:太多的“碰巧工作”和宽容的行为使得很难理解为什么某些东西会起作用,并且很难确定它是否可以移植到其他类型宽度的其他实现中。与 Java 等“安全”语言不同,C 非常不安全,并且在不同平台上具有不同的实现定义的东西,但许多开发人员只有一种实现来测试。(尤其是在互联网和在线持续集成测试之前。)
ISO C 没有定义行为,所以是的,编译器可以将新行为定义为扩展,而不会破坏与任何无 UB 程序的兼容性。但是除非每个编译器都支持它,否则你不能在可移植的 C 程序中使用它。我可以想象它至少是 gcc/clang/ICC 支持的 GNU 扩展。
此外,这样的选项会与-fwrapv定义行为的选项有些冲突。总的来说,我认为它不太可能被采用,因为有方便的语法来指定文字的类型(0x7fffffffUL + 1为您提供一个unsigned long保证足够宽的值作为 32 位无符号整数。)
但是让我们首先将其视为 C 的选择,而不是当前的设计。
一种可能的设计是从以任意精度计算的值推断整个整数常量表达式的类型。为什么是任意精度而不是long longor unsigned long long?如果由于、、 或运算符而导致最终值很小/,则这些对于表达式的中间部分来说可能不够大。>>-&
或者更简单的设计,如 C 预处理器,其中常量整数表达式以某种固定的实现定义宽度(例如至少 64 位)进行计算。(但是然后根据最终值分配类型,还是根据表达式中最宽的临时值?)但这对于 16 位机器上的早期 C 有明显的缺点,它使编译时表达式的计算速度比 if 慢编译器可以在内部为int表达式使用机器的本机整数宽度。
整数常量表达式在 C 中已经有些特殊,需要在某些上下文中在编译时进行评估,例如 for static int array[1024 * 1024 * 1024];(乘法将在 16 位 int 的实现上溢出。)
显然我们不能有效地将提升规则扩展到非常量表达式;如果(a*b)/c可能不得不在 32 位机器上评估a*baslong long而不是int32 位机器,则除法将需要扩展精度。(例如,x86 的 64 位 / 32 位 => 32 位除法指令在商溢出时出错,而不是默默地截断结果,因此即使将结果分配给 anint也不会让编译器在某些情况下进行很好的优化。 )
此外,我们真的想要的行为/ definednessa * b取决于是否a和b有static const或没有? 使编译时评估规则与非常量表达式的规则相匹配,总体而言似乎很好,即使它留下了这些令人讨厌的陷阱。但同样,这是好的编译器可以在常量表达式中警告的事情。
这个 C 问题的其他更常见的情况是,1<<40而不是1ULL << 40定义位标志,或者将 1T 写为1024*1024*1024*1024.
好问题。正如其他人所说,默认情况下数字是int,因此您的操作 fora作用于两个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);
}
Run Code Online (Sandbox Code Playgroud)
编译器确实会警告溢出 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;
^
Run Code Online (Sandbox Code Playgroud)
最后,测试结果符合预期(int第一种情况是溢出long long int,第二种和第三种情况是'):
$ ./test
-2147483648
2147483648
2147483648
Run Code Online (Sandbox Code Playgroud)
另一个 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;
^
Run Code Online (Sandbox Code Playgroud)
另请注意,从技术上讲int,long其变体取决于体系结构,因此它们的位长可能会有所不同。对于可预测大小的类型,您可以更好地使用int64_t,uint32_t等等,这些通常在现代编译器和系统头文件中定义,因此无论您的应用程序是为何种位构建的,数据类型仍然是可预测的。另请注意,此类值的打印和扫描由宏等组成PRIu64。