为什么 unsigned int (1) - unsigned int (0xFFFFFFFF) 的结果是 "2"?

4
请看以下代码:
#include <stdlib.h>
#include <stdio.h>


int main()
{
    unsigned int a = 1;
    unsigned int b = -1;

    printf("0x%X\n", (a-b));

    return 0;
}

结果为0x2。

我认为整数提升不应该发生,因为"a"和"b"的类型都是unsigned int。但是结果却让我感到困惑... 我不知道原因。

顺便说一下,我知道算术结果应该是2,因为1-(-1)=2。但是b的类型是unsigned int。当将(-1)赋给b时,实际上b的值是0xFFFFFFFF。这是unsigned int的最大值。当小的unsigned值减去一个大的值时,结果并不如我所期望。

从下面的答案中,我认为溢出可能是一个很好的解释。现在我写了其他测试代码,证明溢出答案是正确的。

#include <stdlib.h>
#include <stdio.h>

int main()
{
    unsigned int c = 1;
    unsigned int d = -1;


    printf("0x%llx\n", (unsigned long long)c-(unsigned long long)d);

    return 0;
}

结果是“0xffffffff00000002”。这正是我所期望的。

1
谢谢,但我知道算术结果。 - linuxer
4个回答

8
unsigned int a = 1;

这将把a初始化为1。实际上,由于1int类型,存在一个隐式的int转换为unsigned,但它是一个微不足道的转换,不会改变值或表示。

unsigned int b = -1;

这更有趣。"-1" 是 "int" 类型,并且隐式将其从 "int" 转换为 "unsigned int"。由于数学值 "-1" 不能表示成 "unsigned int",因此转换是非平凡的。在这种情况下的规则是(引用C标准第6.3.1.3节):

通过重复添加或减去比新类型中可以表示的最大值大一的值,使该值处于新类型的范围内。

当然,它实际上不必这样做,只要结果相同即可。将 "UINT_MAX+1" (“比新类型中可以表示的最大值大一的值”)加到 "-1" 上会得到 "UINT_MAX"。 (该加法在数学上是定义良好的;本身不受任何类型转换的影响。)
事实上,将 "-1" 分配给任何无符号类型的对象是一种获得该类型的最大值的好方法,而无需引用 在 "<limits.h>" 中定义的 "*_MAX" 宏。
因此,在典型的32位系统上,我们有 "a == 1" 并且 "b == 0xFFFFFFFF"。
printf("0x%X\n", (a-b));

这个减法的数学结果是-0xFFFFFFFE,但这显然超出了unsigned int的范围。无符号运算的规则与无符号转换的规则类似;结果为2。


其实我知道b是0xFFFFFFFF,因为它是无符号的。这个结果是不是由于无符号整数溢出而产生的? - linuxer
4
@linuxer: 我不确定我理解这个问题。严格来讲,无符号类型不会发生溢出或下溢,但这只是对词语意思的一种挑剔。你可以将1 - 0xFFFFFFFF看作是下溢,并得到一个超出unsigned int范围的数学结果,然后进行调整,使其重新回到范围内,得到2。从数学上讲,结果将被模除UINT_MAX+1 - Keith Thompson
我认为如果我可以使用unsigned long long来避免这个问题就好了。例如,我把代码改成了“printf(“0x%llX \ n”,(unsigned long long)(a-b));”。但结果仍然是0x2。 - linuxer
@linuxer: 对于任何无符号类型来说都是一样的(除了在某些情况下unsigned charunsigned short可能会晋升为signed int)。unsigned long long的算术运算按模限制在ULLONG_MAX + 1 - Keith Thompson
因为无符号字符和无符号短整型可能会提升为有符号整型。而且(a-b)的值超出了无符号长整型的范围。所以我想让它提升为无符号长长整型。这应该表示ULONG_MAX+1。 - linuxer
我写了其他的测试代码,并更新了我的帖子。这证明了你的答案是正确的。非常感谢。 - linuxer

3

谁说你遭受整数提升的问题?假装你的整数是二进制补码(可能是这样,但标准没有强制要求),并且它们只有四位宽(根据标准不可能,我只是为了简化事情)。

int   unsigned-int  bit-pattern
---   ------------  -----------
  1              1         0001
 -1             15         1111
                         ------
  (subtract with borrow) 1 0010
   (only have four bits)   0010  -> 2

您可以看到,您可以在不对已签名或更宽的类型进行任何推广的情况下获得所见结果。

改变代码像这样 "printf("0x%llX\n", (unsigned long long)(a-b));",但结果仍然是0x2。 - linuxer
@linuxer,这将使用无符号整数计算a-b,然后将结果转换为更宽的类型。这就是为什么没有区别。 - paxdiablo

2
如果你减去一个负数,相当于加上一个正数。
a = 1
b = -1

(a-b) = ?
((1)-(-1)) = ?
(1-(-1)) = ?
(1+1) = ?
2 = ?

起初,您可能认为这是不允许的,因为您指定了一个unsigned int;但是,您也将signed int(常量-1)转换为unsigned int。因此,您实际上将完全相同的位存储到了无符号整数中(0xFFFF)。
然后,在表达式中,您取0xFFFF值的负数,这当然会强制将该数字转换为有符号数。实际上,您正在每一步中规避unsigned指令。

5
这里发生的不是这样的情况。在减法发生之前,两个数都是正数。 - Merlyn Morgan-Graham
计算机中的减法是用补码完成的,所以在您的硬件上,0xFFFF的补数为1。请参见编辑,您将看到如何在无符号整数中存储,但每一步都强制转回有符号整数。 - Edwin Buck
2
不行。转换和算术是基于而非表示定义的。在一个不使用二进制补码的系统中,将-1int转换为unsigned int仍然会产生UINT_MAX,但它会改变位。在表达式a-b中,两个操作数都是无符号的;没有负值参与。 - Keith Thompson
我同意你的观点,Keith。所以我对结果感到困惑。因为我认为没有任何负值参与其中。 - linuxer

2

你可能忽略或关闭了编译器警告,但仍有可能在无符号整数中存储-1。在32位机器上,内部将-1存储为0xffffffff。因此,如果从1中减去0xffffffff,则得到-0xfffffffe,即2。(没有负数,负数是最大整数值加一减去该数字)。

总之,签名或无符号都无关紧要,只有在比较值时才会发挥作用。

从数学角度来看,1-(-1)= 1 + 1。


1
为什么需要警告?值“-1”从“int”转换为“unsigned int”,结果是明确定义的。 - Keith Thompson
因为这个原因:警告 68:整数转换导致符号改变 - EboMike
嗯,好的。编译器可以警告任何它们想要的东西。 - Keith Thompson
MSVC类似于:warning C4245: 'initializing' : conversion from 'int' to 'unsigned int', signed/unsigned mismatch。这只是一个微妙错误的常见原因,所以大多数编译器都会发出警告。通常只在“挑剔”警告级别下,但那是我首选的级别。 - EboMike

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