负整数与较大的无符号整数相加会被提升为无符号整数吗?

21

得到建议阅读 "C++ Primer 5 ed by Stanley B. Lipman" 后,我不理解这个问题:

第66页,“涉及无符号类型的表达式”

unsigned u = 10;
int i = -42;
std::cout << i + i << std::endl; // prints -84
std::cout << u + i << std::endl; // if 32-bit ints, prints 4294967264

他说:

在第二个表达式中,在执行加法之前,将整数值-42转换为无符号数。将负数转换为无符号数的行为恰好就像我们尝试将该负值分配给无符号对象一样。如上所述,“值会环绕”。

但如果我做这样的事情:

unsigned u = 42;
int i = -10;
std::cout << u + i << std::endl; // Why the result is 32?

正如你所看到的,-10没有被转换为unsigned int。这是否意味着在将有符号整数提升为无符号整数之前发生了比较?


16
正如你所看到的,-10 没有被转换为无符号整数。实际上它已经被转换了。 - tkausl
2
在谷歌中搜索有关二进制数及其表示方式的相关信息,特别是符号位。然后一切都会变得清晰明了。 - DeiDei
2
你期望得到的结果不是32吗? - Barmar
谢谢大家,你们现在已经让我明白了。 - Alex24
5个回答

26

-10 将被转换为一个非常大的无符号整数,你得到一个小数字的原因是加法运算将你回卷了。使用32位无符号整数时,-10 相当于 4294967286。 当你把42加上去,你得到的是 4294967328,但最大值是 4294967296,所以我们必须对 4294967328 取模 4294967296 得到 32


我觉得模算术有趣的地方在于,通过“复杂”的手段,你可以得到与常规算术相同的答案。这是为了处理溢出而证明包装行为的一个原因。 - Matthieu M.
@MatthieuM。只有当您将无符号整数视为数字时,“卷积”才会发生。从等价类42中添加一个数字到等价类-10中的数字,得到的值来自等价类32,这一事实在我看来与有符号结果一样直观。 - Baum mit Augen
非常容易和简单理解,谢谢。有一个问题:为什么 shortchar 没有被提升为 unsigned - Alex24
3
@Alex24 不用谢。shortchar 会被提升为 int,因为这是数学运算符使用的最小内置类型。唯一可能不同的情况是当你有一个 unsigned charunsigned short 并且它与 int 有相同的大小时,它会被提升为 unsigned int - NathanOliver
@NathanOliver:太完美了!再次感谢。我真正理解你的观点了。 - Alex24

18
数据类型。

好吧,我猜这是“锅对锅”不适用的例外情况 :)

发生的情况是,在底层实际上有两个包装(无符号溢出),最终结果在数学上是正确的。

  • 首先,将 i 转换为无符号,并根据包装行为,值为 std::numeric_limits<unsigned>::max() - 9

  • 当此值与 u 相加时,数学上的结果应该是 std::numeric_limits<unsigned>::max() - 9 + 42 == std::numeric_limits<unsigned>::max() + 33,这是一个溢出并产生了另一个包装。因此最终结果是 32


通常情况下,在算术表达式中,如果只有无符号溢出(无论有多少次),并且如果最终的数学结果可以表示为表达式数据类型,则表达式的值将是数学上正确的。这是 C++ 中无符号整数遵守算术模 2n 定律的结果(见下文)。


重要提示。根据 C++ 的规定,无符号算术不会溢出:

§6.9.1 基本类型 [basic.fundamental]

  1. 无符号整数应当遵守对特定值表示的位数为 n 的算术模 2n 定律。
49

49) 这意味着无符号算术不会溢出,因为不能被表示为结果无符号整数类型的结果会对该类型所能表示的最大值加1取模。

我会保留 "overflow" 表示在普通算术中无法表示的值。

此外,我们俗称的 "wrap around" 实际上只是无符号整数的取模算术特性。但是我也会使用 "wrap around",因为这更容易理解。


3
@BaummitAugen 什么?当然会溢出。如果加上两个非常大的无符号整数,就无法表示一个不适合该数字格式的数字。请注意,我已经尽力使翻译准确、通俗易懂,并保持了原意,不包括任何其他解释或信息。 - Garr Godfrey
2
@BaummitAugen 我去看了标准,证明了我的观点是错误的。谢谢。 - bolov
2
@GarrGodfrey 我查了一下标准,他说得没错。这是因为无符号整数不代表自然数,而是像BaummitAugen所说的那样,它们代表等价类。 - bolov
2
@curiousguy 我听说一些人(专家)认为,在无符号数的环绕行为方面做出决策是一个错误。然而,他们主要的原因是它抑制了优化。他们主张对于常规的无符号数,在溢出时应该具有未定义的行为,并存在其他具有环绕行为的数据类型。但事实就是这样。 - bolov
2
“那么为什么有人会使用它们来表示字节数、容器中的对象数量等呢?”委员会的几位重要成员认为这正是一个历史性错误。https://channel9.msdn.com/Events/GoingNative/2013/Interactive-Panel-Ask-Us-Anything 9:50, 42:40, 1:02:50 - Baum mit Augen
显示剩余13条评论

6

i实际上被提升为unsigned int

C和C ++中的无符号整数在ℤ / 2 n ℤ中执行算术运算,其中n是无符号整数类型中的位数。因此我们得到

[42] + [-10] ≡ [42] + [2 n - 10] ≡ [2 n + 32] ≡ [32],

其中[x]表示ℤ / 2 n ℤ中x的等价类。

当然,在每个等价类中仅选择非负代表的中间步骤虽然形式上是必要的,但不必解释结果;即使是直接的

[42] + [-10] ≡ [32]

也是正确的。


3
“在第二个表达式中,整数值-42在执行加法之前被转换为无符号值。”是正确的。 是的,这是真的。
unsigned u = 42;
int i = -10;
std::cout << u + i << std::endl; // Why the result is 32?

假设我们处于32位(这并不影响64位,只是为了解释),这个计算公式为42u + ((unsigned) -10),所以42u + 4294967286u,结果为4294967328u,在32位中截断为32。所有的操作都是无符号的。


2
这就是二进制补码表示法的优点之一。处理器无论数字是有符号还是无符号,操作都是相同的。在这两种情况下,计算都是正确的。只有在打印时,才会实际上关注二进制数的解释方式(可能还有其他情况,比如比较运算符)。请注意保留HTML标签。
-10 in 32BIT binary is FFFFFFF6
42 IN 32bit BINARY is  0000002A

将它们加在一起,对于处理器来说,无论它们是有符号还是无符号,结果都是:100000020。在32位中,开头的1会被放置在溢出寄存器中,在c++中就消失了。你得到的结果是0x20,即32。

在第一个案例中,基本上是一样的:

-42 in 32BIT binary is FFFFFFD6
10 IN 32bit binary is 0000000A

将它们加在一起得到 FFFFFFE0

FFFFFFE0 作为有符号整数是 -32(十进制)。计算是正确的!但是,因为它被打印为无符号数,所以显示为4294967264。这是关于解释结果的问题。


1
顺便提一下,在x86上,大多数算术运算会在标志寄存器中设置各种位,然后可以用于执行分支等操作。例如,加法将设置CF(进位标志)以表示已发生无符号“包装”,并/或OF(溢出标志)以表示已发生有符号溢出,如果您将操作数视为有符号的话。甚至还有一个1字节的INTO指令,如果OF被设置,则生成软件中断,这可以帮助调试,但不幸的是,它不受高级语言的支持,并且在64位模式下不再可用。 - Arne Vogel
1
技术上讲,没有强制要求使用二进制补码表示法。只是标准规定的模算术转换恰好等同于使用二进制补码(如果在正确的符号-幅值或反码系统上执行转换,则会看到这一点)。 - Toby Speight
在1的补码或符号-幅值中实现加法器非常复杂,而在2的补码中则不需要,除非出于安全考虑。 - Garr Godfrey

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接