无符号整数溢出在C和c++标准中都有很好的定义。例如,C99标准(§6.2.5/9)指出

涉及无符号操作数的计算永远不会溢出, 因为不能由结果无符号整数类型表示的结果为 对比最大值大1的数取模 由结果类型表示。

然而,这两个标准都指出有符号整数溢出是未定义的行为。同样,从C99标准(§3.4.3/1)

未定义行为的一个例子是对整数溢出的行为

造成这种差异的原因是历史原因还是技术原因?


历史原因是大多数C实现(编译器)只是使用它所使用的整数表示形式最容易实现的溢出行为。C实现通常使用与CPU相同的表示法-因此溢出行为跟随CPU使用的整数表示法。

在实践中,只有符号值的表示形式可能会因实现的不同而不同:1的补数、2的补数、符号的大小。对于无符号类型,标准没有理由允许变化,因为只有一种明显的二进制表示(标准只允许二进制表示)。

相关报价:

C99 6.2.6.1:3:

存储在unsigned位字段中的值和unsigned char类型的对象应使用纯二进制表示法表示。

C99 6.2.6.2:2:

如果符号位为1,则该值应以以下方式之一进行修改: -与符号位0对应的值被否定(符号和幅度); -符号位的值为−(2N)(2的补码); -符号位的值为−(2N−1)(补码)。


现在,所有处理器都使用2的补数表示,但有符号算术溢出仍然未定义,编译器制作者希望它保持未定义,因为他们使用这种不确定性来帮助优化。例如Ian Lance Taylor的博客文章或Agner Fog的投诉,以及他的错误报告的答案。


除了Pascal的好答案(我相信这是主要的动机),也有可能一些处理器会在有符号整数溢出上引起异常,如果编译器不得不“安排另一种行为”(例如,使用额外的指令来检查潜在的溢出,并在这种情况下计算不同),这当然会导致问题。

It is also worth noting that "undefined behaviour" doesn't mean "doesn't work". It means that the implementation is allowed to do whatever it likes in that situation. This includes doing "the right thing" as well as "calling the police" or "crashing". Most compilers, when possible, will choose "do the right thing", assuming that is relatively easy to define (in this case, it is). However, if you are having overflows in the calculations, it is important to understand what that actually results in, and that the compiler MAY do something other than what you expect (and that this may very depending on compiler version, optimisation settings, etc).


In addition to the other issues mentioned, having unsigned math wrap makes the unsigned integer types behave as abstract algebraic groups (meaning that, among other things, for any pair of values X and Y, there will exist some other value Z such that X+Z will, if properly cast, equal Y and Y-Z will, if properly cast, equal X). If unsigned values were merely storage-location types and not intermediate-expression types (e.g. if there were no unsigned equivalent of the largest integer type, and arithmetic operations on unsigned types behaved as though they were first converted them to larger signed types, then there wouldn't be as much need for defined wrapping behavior, but it's difficult to do calculations in a type which doesn't have e.g. an additive inverse.

这有助于在绕换行为实际有用的情况下—例如TCP序列号或某些算法,如哈希计算。它还可以在需要检测溢出的情况下提供帮助,因为执行计算并检查它们是否溢出通常比事先检查它们是否会溢出更容易,特别是当计算涉及可用的最大整数类型时。


首先,请注意,C11 3.4.3与所有示例和脚注一样,不是规范文本,因此与引用无关!

说明整数和浮点数溢出是未定义行为的相关文本是这样的:

C11 6.5/5

如果在评估过程中出现异常情况 表达式(即,如果结果不是数学上定义的或 不在其类型的可表示值范围内),即行为 是未定义的。

关于无符号整数类型行为的详细说明可以在这里找到:

C11 6.2.5/9

有符号整型的非负值的范围是子范围 对应的无符号整数类型的 每种类型中的相同值都是相同的。计算包括 无符号操作数永远不会溢出,因为结果不能溢出 由结果的无符号整数类型表示的是模数化简 比最大值大1的数 由结果类型表示。

这使得无符号整数类型成为一种特殊情况。

还要注意,如果任何类型被转换为有符号类型,并且旧的值不能再表示,则会出现异常。尽管可能会引发信号,但行为只是由实现定义的。

C11 6.3.1.3

6.3.1.3 Signed and unsigned integers When a value with integer type is converted to another integer type other than _Bool, if the value can be represented by the new type, it is unchanged. Otherwise, if the new type is unsigned, the value is converted by repeatedly adding or subtracting one more than the maximum value that can be represented in the new type until the value is in the range of the new type. Otherwise, the new type is signed and the value cannot be represented in it; either the result is implementation-defined or an implementation-defined signal is raised.


也许无符号算术被定义的另一个原因是因为无符号数是整数对2^n模的形式,其中n是无符号数的宽度。无符号数是用二进制数字而不是十进制数字表示的整数。在模数系统中执行标准操作是很容易理解的。

OP的引用提到了这一事实,但也强调了这样一个事实:在二进制中表示无符号整数只有一种明确的逻辑方法。相比之下,有符号数通常使用2的补数表示,但也可以使用标准中描述的其他选择(第6.2.6.2节)。

Two's complement representation allows certain operations to make more sense in binary format. E.g., incrementing negative numbers is the same that for positive numbers (expect under overflow conditions). Some operations at the machine level can be the same for signed and unsigned numbers. However, when interpreting the result of those operations, some cases don't make sense - positive and negative overflow. Furthermore, the overflow results differ depending on the underlying signed representation.


最技术性的原因很简单,就是试图捕获无符号整数中的溢出需要您(异常处理)和处理器(异常抛出)进行更多的操作。

C和c++不会让你为此付出代价,除非你使用有符号整数来请求它。这不是一个严格的规则,正如您将在接近结尾时看到的,而是它们如何处理无符号整数。在我看来,这使得有符号整数被排除在外,而不是无符号整数,但它们提供了这一基本区别,因为程序员仍然可以使用overflow执行定义良好的有符号操作。但要做到这一点,你必须把它投进去。

因为:

无符号整数具有明确定义的溢出和下溢 从signed -> unsigned int的类型转换是定义良好的,[uint的名称]_MAX - 1在概念上被添加到负值,以将它们映射到扩展的正数范围 [uint的名字]_MAX - 1在概念上是从带符号类型的最大值以外的正数值中扣除,以将它们映射到负数)

您总是可以执行具有定义良好的溢出和下溢行为的算术操作,其中有符号整数是您的起点,尽管是以一种迂回的方式,通过先转换为无符号整数,然后在完成后返回。

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的补数(几乎所有都是这样),那么相同宽度的有符号整型和无符号整型之间的强制类型转换是自由的。如果由于某种原因,你的目标平台没有对有符号整数使用2的Compliment,那么当你在uint32和int32之间进行强制转换时,你将付出一个小的转换代价。

但是当使用比特宽度小于int时要小心

通常,如果您依赖于无符号溢出,则使用较小的字宽,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