和 C++ 标准都很好地定义了无符号整数溢出。例如,C99 standard (§6.2.5/9
) 状态
涉及无符号操作数的计算永远不会溢出,因为无法由结果无符号整数类型表示的结果会以比结果类型可以表示的最大值大一的数字为模减少。
但是,这两个标准都指出有符号整数溢出是未定义的行为。同样,来自 C99 标准 (§3.4.3/1
)
未定义行为的一个例子是整数溢出行为
这种差异是否有历史或(甚至更好!)技术原因?
if (a + b < a)
)。有符号和无符号类型的乘法溢出都很难。
MAX_INT+1 == -0
,而在二进制补码中,它将是 INT_MIN
历史原因是大多数 C 实现(编译器)只是使用了最容易通过它使用的整数表示来实现的溢出行为。 C 实现通常使用 CPU 使用的相同表示 - 因此溢出行为遵循 CPU 使用的整数表示。
在实践中,只有符号值的表示可能会根据实现而有所不同:一个补码、二进制补码、符号幅度。对于无符号类型,标准没有理由允许变化,因为只有一种明显的二进制表示(标准只允许二进制表示)。
相关报价:
C99 6.2.6.1:3:
存储在无符号位域和无符号字符类型对象中的值应使用纯二进制表示法表示。
C99 6.2.6.2:2:
如果符号位为 1,则应通过以下方式之一修改该值: — 符号位为 0 的对应值取反(符号和幅度); — 符号位的值为 -(2N)(二进制补码); — 符号位的值为 -(2N - 1)(一个补码)。
如今,所有处理器都使用二进制补码表示,但有符号算术溢出仍未定义,编译器制造商希望它保持未定义,因为他们使用这种未定义来帮助优化。例如,请参阅 Ian Lance Taylor 的此 blog post 或 Agner Fog 的此 complaint,以及他的错误报告的答案。
除了 Pascal 的好答案(我确信这是主要动机)之外,某些处理器也可能导致有符号整数溢出异常,如果编译器必须“安排另一种行为”,这当然会导致问题(例如,使用额外的指令来检查潜在的溢出并在这种情况下进行不同的计算)。
还值得注意的是,“未定义的行为”并不意味着“不起作用”。这意味着允许实现在这种情况下做任何它喜欢的事情。这包括做“正确的事”以及“报警”或“撞车”。大多数编译器会在可能的情况下选择“做正确的事”,假设它相对容易定义(在这种情况下,确实如此)。但是,如果您在计算中出现溢出,重要的是要了解实际结果是什么,并且编译器可能会做一些您期望之外的事情(这可能很大程度上取决于编译器版本、优化设置等) .
int f(int x) { return x+1>x; }
时立即向您展示。 GCC 和 ICC 使用默认选项将上述优化为 return 1;
。
int
溢出时给出不同结果的示例程序,请参阅 ideone.com/cki8nM 我认为这表明您的答案给出了不好的建议。
首先,请注意 C11 3.4.3 与所有示例和脚注一样,不是规范性文本,因此与引用无关!
说明整数和浮点数溢出是未定义行为的相关文本是:
C11 6.5/5
如果在计算表达式期间出现异常情况(即,如果结果未在数学上定义或不在其类型的可表示值范围内),则行为未定义。
关于无符号整数类型的行为的说明可以在这里找到:
C11 6.2.5/9
有符号整数类型的非负值范围是对应无符号整数类型的子范围,相同值在每种类型中的表示是相同的。涉及无符号操作数的计算永远不会溢出,因为无法由结果无符号整数类型表示的结果会以比结果类型可以表示的最大值大一的数字为模减少。
这使得无符号整数类型成为一种特殊情况。
另请注意,如果将任何类型转换为有符号类型并且无法再表示旧值,则会出现异常。然后行为仅由实现定义,尽管可能会引发信号。
C11 6.3.1.3
6.3.1.3 有符号和无符号整数 当一个整数类型的值转换为_Bool 以外的其他整数类型时,如果该值可以用新的类型表示,则保持不变。否则,如果新类型是无符号的,则通过在新类型中可以表示的最大值的基础上反复加减一,直到该值在新类型的范围内。否则,新类型是有符号的,值不能在其中表示;结果是实现定义的,或者引发了实现定义的信号。
除了提到的其他问题之外,无符号数学包装使无符号整数类型表现为抽象代数组(这意味着,除其他外,对于任何一对值 X
和 Y
,将存在一些其他值 { 3} 使得 X+Z
如果正确转换,将等于 Y
并且 Y-Z
如果正确转换,将等于 X
)。如果无符号值仅仅是存储位置类型而不是中间表达式类型(例如,如果没有最大整数类型的无符号等价物,并且对无符号类型的算术运算表现得好像它们首先被转换为更大的有符号类型,那么不需要定义的包装行为,但是很难在没有例如加法逆的类型中进行计算。
这在环绕行为实际上有用的情况下有所帮助 - 例如使用 TCP 序列号或某些算法,如哈希计算。在需要检测溢出的情况下它也可能有所帮助,因为执行计算并检查它们是否溢出通常比预先检查它们是否会溢出更容易,特别是如果计算涉及最大的可用整数类型。
a+b-c
是在循环内计算的,但 b
和 c
在该循环内是常数,则将 (b-c)
的计算移到循环外可能会有所帮助,但这样做需要(b-c)
产生一个值的其他事物,当添加到 a
时,将产生 a+b-c
,这反过来又要求 c
有一个加法逆。
(a+b)-c
等于 a+(b-c)
,无论 b-c
的算术值是否可在类型内表示,无论 (b-c)
值的可能范围如何,替换都是有效的。
定义无符号算术的另一个原因可能是因为无符号数形成模 2^n 的整数,其中 n 是无符号数的宽度。无符号数只是使用二进制数字而不是十进制数字表示的整数。在模数系统中执行标准操作是很好理解的。
OP 的引用提到了这一事实,但也强调了这样一个事实,即只有一种明确的、合乎逻辑的方式来表示二进制中的无符号整数。相比之下,有符号数最常使用二进制补码表示,但如标准中所述(第 6.2.6.2 节),其他选择也是可能的。
二进制补码表示允许某些操作在二进制格式中更有意义。例如,增加负数与正数相同(在溢出条件下除外)。对于有符号数和无符号数,机器级别的某些操作可能相同。但是,在解释这些操作的结果时,有些情况是没有意义的——正溢出和负溢出。此外,溢出结果因底层有符号表示而异。
最技术性的原因只是试图捕获无符号整数中的溢出需要您(异常处理)和处理器(异常抛出)更多的移动部分。
和 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 位。这些将立即升级为 signed 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
不定期副业成功案例分享