为什么无符号整数溢出定义行为,但有符号整数溢出是n't?

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

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

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

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

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

59259 次浏览

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

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

相关报价:

C99 6.2.6.1:3:

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

C99 6.2.6.2:2:

如果符号位为1,则该值应以以下方式之一进行修改:

-符号位为0的对应值为负(符号和幅度);

-符号位的值为−(2N) (二进制补码);

—符号位的值为−(2N−1)(一个人的补充)。


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

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

同样值得注意的是,“未定义的行为”并不意味着“不起作用”。这意味着在这种情况下,实现可以做任何它喜欢做的事情。这包括做“正确的事情”以及“报警”或“撞车”。大多数编译器,在可能的情况下,会选择“做正确的事情”,假设这是相对容易定义的(在这种情况下,它是)。然而,如果你在计算中有溢出,重要的是要了解实际结果是什么,编译器可能会做一些与你期望的不同的事情(这可能非常依赖于编译器版本,优化设置等)。

除了上面提到的其他问题外,无符号数学换行使得无符号整数类型表现为抽象代数组(这意味着,除其他外,对于任何值XY,将存在一些其他值Z,使得X+Z如果正确转换,将等于Y,而Y-Z如果正确转换,将等于X)。如果无符号值仅仅是存储位置类型而不是中间表达式类型(例如,如果没有最大整数类型的无符号等价物,并且对无符号类型的算术操作表现得好像首先将它们转换为更大的有符号类型,那么就不太需要定义包装行为,但是在没有例如加性逆的类型中进行计算是困难的。

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

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

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

C11 6.5/5

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

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

C11 6.2.5/9

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

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

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

C11 6.3.1.3

6.3.1.3有符号整数和无符号整数

当值为整型时 类型转换为_Bool类型以外的其他整数类型,如果 值可以用新类型表示,它是不变的。< / p >

否则,如果新类型为unsigned,则将值转换为 比最大值反复加或减一 可以在新类型中表示,直到值在 新型的。< / p >

否则,新类型是有符号的,值是 不能在其中表现出来;结果是 触发实现定义的信号或实现定义的信号

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

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

Two的补码表示法允许某些操作在二进制格式中更有意义。例如,对负数递增和对正数递增是一样的(在溢出条件下除外)。对于有符号数和无符号数,机器级别上的一些操作可以是相同的。然而,在解释这些操作的结果时,有些情况是没有意义的——正溢出和负溢出。此外,溢出结果因底层有符号表示的不同而不同。

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

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

因为:

  • 无符号整数具有明确定义的溢出和下溢
  • 从有符号的->unsigned int是定义良好的,[uint's name]_MAX - 1在概念上被添加到负值,以将它们映射到扩展的正数范围
  • 从unsigned ->有符号int是定义良好的,[uint's name]_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