242

C 和 C++ 标准都很好地定义了无符号整数溢出。例如,C99 标准( §6.2.5/9) 规定

涉及无符号操作数的计算永远不会溢出,因为无法由结果无符号整数类型表示的结果会以比结果类型可以表示的最大值大一的数字为模减少。

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

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

这种差异是否有历史或(甚至更好!)技​​术原因?

4

6 回答 6

182

历史原因是大多数 C 实现(编译器)只是使用了最容易通过它使用的整数表示来实现的溢出行为。C 实现通常使用 CPU 使用的相同表示 - 因此溢出行为遵循 CPU 使用的整数表示。

在实践中,只有符号值的表示可能会根据实现而有所不同:一个补码、二进制补码、符号幅度。对于无符号类型,标准没有理由允许变化,因为只有一种明显的二进制表示(标准只允许二进制表示)。

相关报价:

C99 6.2.6.1:3

存储在无符号位域和无符号字符类型对象中的值应使用纯二进制表示法表示。

C99 6.2.6.2:2

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

— 符号位为 0 的对应值取反(符号和幅度);

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

— 符号位的值是 −(2 N − 1)(反码)。


如今,所有处理器都使用二进制补码表示,但有符号算术溢出仍未定义,编译器制造商希望它保持未定义,因为他们使用这种未定义来帮助优化。例如,请参阅Ian Lance Taylor 的这篇博客文章或 Agner Fog 的这篇投诉,以及他的错误报告的答案。

于 2013-08-12T20:06:48.813 回答
18

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

还值得注意的是,“未定义的行为”并不意味着“不起作用”。这意味着允许实现在这种情况下做任何它喜欢的事情。这包括做“正确的事”以及“报警”或“撞车”。大多数编译器会在可能的情况下选择“做正确的事”,假设它相对容易定义(在这种情况下,确实如此)。但是,如果您在计算中出现溢出,重要的是要了解实际结果是什么,并且编译器可能会做一些您期望之外的事情(这可能很大程度上取决于编译器版本、优化设置等) .

于 2013-08-12T20:10:43.150 回答
11

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

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

C11 6.5/5

如果在计算表达式期间出现异常情况(即,如果结果未在数学上定义或不在其类型的可表示值范围内),则行为未定义。

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

C11 6.2.5/9

有符号整数类型的非负值范围是对应无符号整数类型的子范围,相同值在每种类型中的表示是相同的。涉及无符号操作数的计算永远不会溢出,因为无法由结果无符号整数类型表示的结果会以比结果类型可以表示的最大值大一的数字为模减少。

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

另请注意,如果将任何类型转换为有符号类型并且无法再表示旧值,则会出现异常。然后行为仅由实现定义,尽管可能会引发信号。

C11 6.3.1.3

6.3.1.3 有符号和无符号整数

当整数类型的值转换为_Bool以外的其他整数类型时,如果该值可以用新类型表示,则保持不变。

否则,如果新类型是无符号的,则通过在新类型中可以表示的最大值的基础上反复加减一,直到该值在新类型的范围内。

否则,新类型是有符号的,值不能在其中表示;结果是实现定义的,或者引发了实现定义的信号。

于 2016-12-22T09:53:33.370 回答
7

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

这在环绕行为实际上有用的情况下有所帮助 - 例如使用 TCP 序列号或某些算法,如哈希计算。在需要检测溢出的情况下,它也可能有所帮助,因为执行计算并检查它们是否溢出通常比预先检查它们是否会溢出更容易,尤其是当计算涉及最大的可用整数类型时。

于 2013-08-26T17:31:59.857 回答
3

定义无符号算术的另一个原因可能是因为无符号数形成模 2^n 的整数,其中 n 是无符号数的宽度。无符号数只是使用二进制数字而不是十进制数字表示的整数。在模数系统中执行标准操作是很好理解的。

OP 的引用提到了这一事实,但也强调了这样一个事实,即只有一种明确的、合乎逻辑的方式来表示二进制中的无符号整数。相比之下,有符号数最常使用二进制补码表示,但如标准中所述(第 6.2.6.2 节),其他选择也是可能的。

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

于 2017-10-04T19:24:14.723 回答
0

最技术性的原因只是试图捕获无符号整数中的溢出需要您(异常处理)和处理器(异常抛出)更多的移动部分。

C 和 C++ 不会让您为此付费,除非您使用有符号整数来要求它。正如您将在结尾处看到的那样,这不是一个硬性规则,而是它们如何处理无符号整数。在我看来,这使得有符号整数成为奇数,而不是无符号整数,但它们提供了这种根本差异,因为程序员仍然可以执行定义明确的有符号操作并溢出。但要做到这一点,你必须为此而努力。

因为:

  • 无符号整数具有明确定义的上溢和下溢
  • 来自signed -> unsigned int 的强制转换定义明确,[uint's name]_MAX - 1在概念上添加到负值,以将它们映射到扩展的正数范围
  • 来自 unsigned -> signed 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
于 2021-08-27T04:24:54.467 回答