ChatGPT解决这个技术问题 Extra ChatGPT

为什么定义了无符号整数溢出行为但没有定义有符号整数溢出?

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

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

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

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

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

可能是因为有不止一种表示有符号整数的方法。标准中没有指定哪种方式,至少在 C++ 中没有。
juanchopanza 说的很有道理。据我了解,最初的 C 标准在很大程度上编纂了现有实践。如果当时所有的实现都同意无符号的“溢出”应该做什么,那就是让它标准化的一个很好的理由。他们没有就签名溢出应该做什么达成一致,因此没有进入标准。
@DavidElliman 添加的无符号环绕也很容易检测到(if (a + b < a))。有符号和无符号类型的乘法溢出都很难。
@DavidElliman:这不仅是您是否可以检测到它的问题,还在于结果是什么。在符号 + 值实现中,MAX_INT+1 == -0,而在二进制补码中,它将是 INT_MIN

C
Community

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


不过,这里重要的一点是,在现代世界中,除了 2 的补码有符号算术之外,还没有其他架构。语言标准仍然允许在例如 PDP-1 上实现是纯粹的历史产物。
@AndyRoss,但截至 2013 年仍有一些系统(OS + 编译器,诚然具有悠久的历史)具有补充和新版本。例如:OS 2200。
@Andy Ross 您是否会考虑“没有架构......使用除 2 的补码以外的任何东西......”今天包括 DSP 和嵌入式处理器的范围?
@AndyRoss:虽然“没有”架构使用除 2s 补码以外的任何东西(对于“否”的某些定义),但肯定有 DSP 架构使用饱和算法来处理有符号整数。
饱和有符号算术绝对符合标准。当然,包装指令必须用于无符号算术,但编译器总是有信息知道是否正在执行无符号或有符号算术,因此它当然可以适当地选择指令。
T
Toby Speight

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

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


但是,编译器不希望您依赖它们做正确的事情,而且它们中的大多数会在您通过优化编译 int f(int x) { return x+1>x; } 时立即向您展示。 GCC 和 ICC 使用默认选项将上述优化为 return 1;
有关根据优化级别在面临 int 溢出时给出不同结果的示例程序,请参阅 ideone.com/cki8nM 我认为这表明您的答案给出了不好的建议。
我对那部分做了一些修改。
如果 C 提供了一种声明“包装有符号二进制补码”整数的方法,那么任何可以运行 C 的平台都不应该在支持它时遇到很多麻烦,至少可以适度有效地支持它。额外的开销足以使代码在不需要包装行为时不使用这种类型,但是除了比较和提升之外,对二进制补码整数的大多数操作与对无符号整数的操作相同。
负值需要存在并且“工作”才能使编译器正常工作,当然完全有可能解决处理器中缺少有符号值的问题,并使用无符号值作为一个补码或二进制补码,以最根据指令集是什么来感知的。这样做通常比为其提供硬件支持要慢得多,但它与不支持硬件浮点或类似的处理器没有什么不同——它只是增加了很多额外的代码。
L
Lundin

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

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

C11 6.5/5

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

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

C11 6.2.5/9

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

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

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

C11 6.3.1.3

6.3.1.3 有符号和无符号整数 当一个整数类型的值转换为_Bool 以外的其他整数类型时,如果该值可以用新的类型表示,则保持不变。否则,如果新类型是无符号的,则通过在新类型中可以表示的最大值的基础上反复加减一,直到该值在新类型的范围内。否则,新类型是有符号的,值不能在其中表示;结果是实现定义的,或者引发了实现定义的信号。


s
supercat

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

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


我不太明白 - 为什么有一个加法逆运算有帮助?我真的想不出溢出行为实际上有用的任何情况......
@sleske:使用十进制来提高人类可读性,如果电能表读数为 0003 而之前的读数为 9995,这是否意味着使用了 -9992 单位的能源,或者使用了 0008 单位的能源?具有 0003-9995 产生 0008 可以很容易地计算后一个结果。让它产生 -9992 会让它有点尴尬。但是,如果不能让它做,则需要将 0003 与 9995 进行比较,注意它更小,进行反向减法,从 9999 中减去该结果,然后加 1。
@sleske:对于人类和编译器来说,能够应用算术的关联、分配和交换律来重写表达式并简化它们也非常有用;例如,如果表达式 a+b-c 是在循环内计算的,但 bc 在该循环内是常数,则将 (b-c) 的计算移到循环外可能会有所帮助,但这样做需要(b-c) 产生一个值的其他事物,当添加到 a 时,将产生 a+b-c,这反过来又要求 c 有一个加法逆。
: 谢谢你的解释。如果我理解正确,您的示例都假定您实际上想要处理溢出。在我遇到的大多数情况下,溢出是不可取的,你想防止它,因为溢出计算的结果是没有用的。例如,对于电能表,您可能希望使用不会发生溢出的类型。
...这样 (a+b)-c 等于 a+(b-c),无论 b-c 的算术值是否可在类型内表示,无论 (b-c) 值的可能范围如何,替换都是有效的。
b
bjarchi

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

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

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


对于要成为域的结构,除了加法恒等式之外,该结构的每个元素都必须具有乘法逆元。只有当 N 为 1 或素数时,整数全等 mod N 的结构才会是一个域[当 N==1 时的退化域]。你觉得我在回答中遗漏了什么吗?
你说的对。我对主要功率模量感到困惑。原始回复已编辑。
这里更令人困惑的是,有一个 2^n 阶的字段,它与以 2^n 为模的整数不是环同构的。
而且,2^31-1 是梅森素数(但 2^63-1 不是素数)。就这样,我原来的想法破灭了。此外,整数大小在当时是不同的。所以,我的想法充其量是修正主义的。
事实上,无符号整数形成一个环(不是一个字段),取低位部分也产生一个环,并且对整个值执行操作然后截断将等同于仅对较低部分执行操作,恕我直言几乎可以肯定的考虑。
A
Anne Quinn

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

和 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

“试图捕获无符号整数中的溢出需要更多的移动部分”你的意思是有符号的?
“来自 unsigned -> signed int 的强制转换定义明确”:这是不正确的;如果结果不能以有符号类型表示,则从无符号转换为有符号会产生实现定义的结果。 (或引发实现定义的信号。)大多数实现确实按照您的描述进行包装,但标准不保证。 C17 6.3.1.3p3