为什么未定义整数溢出定义行为,但没有定义整数溢出?


209

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

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

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

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

是否存在这种差异的历史原因(甚至更好!)是技术原因?


50
可能是因为存在不止一种表示有符号整数的方法。标准中未指定哪种方式,至少C ++中未指定。
juanchopanza


7
juanchopanza所说的是有道理的。据我了解,原始的C标准在很大程度上编纂了现有的做法。如果当时所有实现都同意应执行未签名的“溢出”,那就是使其标准化的一个很好的理由。他们对于签名溢出应该怎么做并没有达成共识,因此并没有成为标准。

2
@DavidElliman添加时的无符号环绕也很容易检测到(if (a + b < a))。对于有符号和无符号类型,乘法溢出都是困难的。

5
@DavidElliman:这不仅是您是否可以检测到它的问题,而且是什么结果。有迹象+价值的实现,MAX_INT+1 == -0而在两个补这将是INT_MIN
大卫·罗德里格斯- dribeas

Answers:


163

历史上的原因是,大多数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的投诉,以及他的错误报告的答案。


6
这里重要的注意,不过,是仍然存在使用除签署算术2的补以外的任何其他架构在现代世界。语言标准仍允许在例如PDP-1上实现,这是纯粹的历史产物。
安迪·罗斯

9
@AndyRoss,但仍然有一些系统(OS +编译器,具有悠久的历史),截至2013
已有

3
@Andy Ross,您今天是否会考虑“没有架构……使用除2的补码之外的任何东西……”包括DSP和嵌入式处理器的全部范围?
chux-恢复莫妮卡

11
@AndyRoss:虽然有“ no”架构使用2s补码之外的任何东西(对于“ no”的某种定义),但肯定有 DSP架构对符号整数使用饱和算法。
斯蒂芬·佳能

10
饱和带符号算术绝对符合该标准。当然,包装指令必须用于无符号算术,但是编译器始终具有信息来知道是在执行无符号算术还是有符号算术,因此,它肯定可以适当地选择指令。
caf

15

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

还值得注意的是,“未定义的行为”并不意味着“不起作用”。这意味着在这种情况下,允许实现执行任何操作。这包括做“正确的事情”以及“报警”或“崩溃”。假定可能相对容易定义(在本例中是这样),大多数编译器将在可能的情况下选择“做正确的事”。但是,如果您在计算中存在溢出,那么重要的是要了解实际结果,并且编译器可能会执行超出您期望的操作(这可能取决于编译器版本,优化设置等)。 。


23
但是,编译器不希望您依靠他们做正确的事,并且大多数编译器会在您int f(int x) { return x+1>x; }进行优化后立即向您显示。GCC和ICC使用默认选项将上述内容优化为return 1;
Pascal Cuoq 2013年

1
有关一个示例程序,该程序在遇到int溢出时会根据优化级别给出不同的结果,请参阅ideone.com/cki8nM。我认为这表明您的答案给出了错误的建议。
Magnus Hoff 2013年

我已经对该部分进行了一些修改。
Mats Petersson

如果C提供声明“包装有符号的二进制补码”整数的方法,则任何可以运行C的平台都应该在支持中等效率方面遇到很多麻烦。多余的开销足以使代码在不需要包装行为时不应该使用这种类型,但是除了比较和提升外,大多数对二进制补码整数的操作与对无符号整数的操作相同。
supercat 2014年

1
必须存在负值并“起作用”,编译器才能正常工作。当然,完全有可能解决处理器中缺少带符号值的问题,并使用无符号值(作为一个补码或两个补码),以最有效的方式为准。基于指令集的意义。通常,执行此操作要比对其提供硬件支持慢得多,但与不支持硬件浮点或类似功能的处理器没有什么不同-它只是添加了很多额外的代码。
Mats Petersson,2015年

10

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

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

C11 6.5 / 5

如果在表达式的求值过程中发生异常情况(即,如果未在数学上定义结果或该结果不在其类型的可表示值范围内),则该行为不确定。

可以在以下位置找到有关无符号整数类型的行为的明确说明:

C11 6.2.5 / 9

有符号整数类型的非负值范围是相应的无符号整数类型的子范围,并且每种类型中相同值的表示形式相同。涉及无符号操作数的计算永远不会溢出,因为无法用所得的无符号整数类型表示的结果的模数要比该所得的类型可以表示的最大值大一模。

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

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

C11 6.3.1.3

6.3.1.3有符号和无符号整数

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

否则,如果新类型是无符号的,则通过重复添加或减去比新类型可表示的最大值多一个值来转换该值,直到该值在新类型的范围内为止。

否则,将对新类型进行签名,并且无法在其中表示值;结果是实现定义的,还是引发实现定义的信号。


6

除了其他问题所提到的,具有无符号数包,使无符号整数类型表现为抽象代数群(这意味着,除其他事项外,对于任何一对值XY,将存在一些其他的价值Z,使得X+Z意志,如果得到适当的投,等于)。如果无符号值仅仅是存储位置类型,而不是中间表达式类型(例如,如果不存在最大整数类型的无符号等效项,并且对无符号类型的算术运算的行为就好像首先将它们转换为较大的有符号类型,则存在对定义的包装行为的需求不是很大,但是很难在没有例如加法逆的类型中进行计算。YY-Z意志,如果适当地投,等于X

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


我不太了解-为什么加法逆运算会有所帮助?我真的想不出溢出行为实际上有用的任何情况……
sleske

@sleske:如果电能表读数为0003,而以前的读数为9995,则使用十进制来表示人类可读性,这是否意味着使用了-9992单位能量,或者使用了0008单位能量?0003-9995的结果为0008,可以很容易地计算出后者的结果。产生-9992会使它更加尴尬。但是,如果无法执行此操作,则必须将0003与9995进行比较,注意它要少一些,进行反向减法,从9999中减去结果,然后加
1。– supercat

@sleske:对于人类和编译器来说,能够应用算术的关联,分布和可交换定律来重写表达式并简化它们也非常有用;例如,如果表达式a+b-c是一个循环中计算的,但bc该循环内是恒定的,它可能是有帮助的移动的计算(b-c)外循环,但这样做将要求中,其它的东西(b-c)产生,当加入到一个值a,将产生a+b-c,这又需要c具有加法逆。
supercat

:感谢您的解释。如果我理解正确,那么所有示例均假定您确实要处理溢出。在大多数情况下,我不希望发生溢出,并且您想防止它发生,因为带有溢出的计算结果没有用。例如,对于电表,您可能希望使用一种不会发生溢出的类型。
sleske

1
... (a+b)-c等于等于a+(b-c)的算术值b-c在类型内是否可表示,无论的可能取值范围如何,替换都将有效(b-c)
超级猫

1

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

OP的引用引用了这一事实,但同时也强调了这样一个事实,即只有一种明确的逻辑方式可以表示二进制无符号整数。相反,带符号的数字通常使用二进制补码表示,但如标准(第6.2.6.2节)中所述,其他选择也是可能的。

二进制补码表示允许某些操作以二进制格式更有意义。例如,递增负数与正数相同(在溢出条件下预期)。对于有符号和无符号数字,计算机级别的某些操作可以相同。但是,在解释这些运算的结果时,有些情况没有意义-正向和负向溢出。此外,溢出结果根据基础签名表示而有所不同。


为了使一个结构成为一个字段,该结构的每个元素(除了加法标识)都必须具有乘法逆。整数一致的模N的结构仅在N为1或素数时才是字段(当N == 1时为退化的字段)。您有什么感觉我错过了答案吗?
超级猫

你是对的。我对主要功率模数感到困惑。原始回复已编辑。
yth

额外这里混乱的是,有 2阶^ N场,到整数模2 ^ N它是不环同构。
凯文·文图洛

而且,2 ^ 31-1是Mersenne Prime(但2 ^ 63-1不是质数)。因此,我的初衷被毁了。同样,整型大小在一天中也有所不同。因此,我的想法充其量是修正主义者。
yth

恕我直言,无符号整数形成一个环(不是字段),取低阶部分也会产生一个环,并且对整个值执行运算然后截断的事实等同于仅对下部执行运算几乎可以肯定的考虑因素。
超级
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.