无符号整数溢出在C和C++标准中都有明确定义。例如,C99标准(§6.2.5/9
)规定:
涉及无符号操作数的计算永远不会溢出,因为不能用结果类型表示的结果将对能够表示的结果类型最大值加1取模
然而,两个标准都声明有符号整数溢出是未定义的行为。同样来自C99标准 (§3.4.3/1
):
未定义行为的一个例子是整数溢出时的行为
这种差异存在历史原因或(更好的!)技术原因吗?
无符号整数溢出在C和C++标准中都有明确定义。例如,C99标准(§6.2.5/9
)规定:
涉及无符号操作数的计算永远不会溢出,因为不能用结果类型表示的结果将对能够表示的结果类型最大值加1取模
然而,两个标准都声明有符号整数溢出是未定义的行为。同样来自C99标准 (§3.4.3/1
):
未定义行为的一个例子是整数溢出时的行为
这种差异存在历史原因或(更好的!)技术原因吗?
除了Pascal的好回答(我相信这是主要动力),还有可能是一些处理器在有符号整数溢出时会引发异常,这当然会导致问题,如果编译器必须"安排另一种行为"(例如使用额外的指令来检查潜在溢出并在这种情况下进行不同的计算)。
值得注意的是,“未定义的行为”并不意味着“无法工作”。它意味着实现可以在这种情况下做任何它想做的事情。这包括做“正确的事情”以及“呼叫警方”或“崩溃”。大多数编译器在可能的情况下会选择“做正确的事情”,假设相对容易定义(在这种情况下是如此)。但是,如果您的计算中存在溢出,了解其实际结果以及编译器可能执行与预期不同的操作很重要(而这可能会因编译器版本、优化设置等而异)。
int f(int x) { return x+1>x; }
后将其优化为 return 1;
,因此告诉你不能盲目依赖编译器。其中GCC和ICC是默认采用这种优化方式的。 - Pascal Cuoqint
溢出时会给出不同的结果。我认为这证明了你的回答给出了错误的建议。 - Magnus HoffX
和Y
任意一对值,都将存在某些其他值Z
,使得X+Z
如果适当地转换,将等于Y
,而Y-Z
如果适当地转换,将等于X
)。如果无符号值仅仅是存储位置类型而不是中间表达式类型(例如,如果没有最大整数类型的无符号等效类型,并且对无符号类型进行算术运算就像它们首先被转换为较大的有符号类型一样,则不需要定义环绕行为,但在没有可加逆元素的情况下进行计算很难。
这在实际使用中具有环绕行为非常有用——例如,在TCP序列号或某些算法(如哈希计算)中。它也可能在需要检测溢出的情况下有所帮助,因为执行计算并检查是否溢出通常比预先检查是否会溢出更容易,特别是如果计算涉及最大可用整数类型。
(a+b)-c
等于a+(b-c)
,无论b-c
的算术值是否可在该类型中表示,替换都将是有效的,而且不受(b-c)
可能取值范围的影响。 - supercat最主要的技术原因是,尝试在无符号整数中捕获溢出需要更多的移动部件(异常处理)和处理器(异常抛出)。
C和C++不会让你为此付出代价,除非你使用有符号整数。这并不是一个硬性规定,正如你将在最后看到的那样,但这就是它们处理无符号整数的方式。在我看来,这使得有符号整数成为了奇怪的存在,而不是无符号整数,但是他们提供这种基本差异是可以的,程序员仍然可以执行具有溢出的明确定义的有符号操作。但是,为此必须进行强制转换。
因为:
您可以始终使用具有明确定义的溢出和下溢行为的算术运算,其中有符号整数是您的起点,尽管需要通过先转换为无符号整数,然后在完成后再转回来。
int32_t x = 10;
int32_t y = -50;
// writes -60 into z, this is well defined
int32_t z = int32_t(uint32_t(y) - uint32_t(x));
如果CPU使用2的补码(几乎所有CPU都是如此),则在相同宽度的有符号和无符号整数类型之间进行转换是免费的。但是,如果您的目标平台出于某种原因不使用2的补码来表示有符号整数,则在uint32和int32之间进行转换时将付出一些转换代价。
通常,如果您依赖于无符号溢出,那么您正在使用较小的字宽,例如8位或16位。这些将在任何时候都升级为有符号int
(C具有绝对疯狂的隐式整数转换规则,这是C最大的隐藏陷阱之一),请考虑:
unsigned char a = 0;
unsigned char b = 1;
printf("%i", a - b); // outputs -1, not 255 as you'd expect
unsigned char a = 0;
unsigned char b = 1;
printf("%i", (unsigned char)(a - b)); // cast turns -1 to 255, outputs 255
C++只是从C中继承了这种行为。
我认为,在C语言的使用者和实现者之间已经存在了一种脱节。C语言最初被设计为汇编语言的可移植替代品,最初并没有像现在这样的标准,只有一本描述该语言的书籍。在早期的C语言中,低级平台特定的黑客技巧是常见且被接受的做法。许多真正的C程序员仍然认为C语言是这样的。
当引入标准时,其目标主要是标准化现有的做法。有些事情被留空或者是实现定义的。我不确定有多少注意力被放在了哪些东西是未定义的,哪些东西是实现定义的上。
在C语言标准化时,二进制补码是最常见的方法,但其他方法也存在,因此C语言不能直接要求使用二进制补码。
如果您阅读https://www.open-std.org/jtc1/sc22/wg14/www/C99RationaleV5.10.pdf中关于标准C的原理解释,他们讨论了提升语义的选择,他们决定采用“值保留”的语义更安全,但是他们基于这样的假设做出了这个决定,即大多数实现使用二进制补码,并以明显的方式静默处理环绕。
然而,编译器供应商在某个时候开始将有符号溢出视为优化机会。这已经将有符号溢出变成了一个主要的陷阱。除非您仔细检查每个算术操作以确保它不会溢出,否则可能会触发未定义行为。
一旦触发了未定义行为,“任何事情都可能发生”。实际上,这意味着变量实际包含的值可能超出编译器认为它可以包含的值范围。这反过来又可能使边界检查无效。
if (a + b < a)
)。有符号和无符号类型的乘法溢出都很难检测。 - user743382MAX_INT+1 == -0
,而在二进制补码表示法中则为INT_MIN
。 - David Rodríguez - dribeas