你所看到的是由于C标准定义了有符号类型转换为无符号类型(用于算术运算)以及底层机器如何表示数字(用于未定义行为结果)的结果。
当我最初回答时,我假设C标准没有明确定义有符号值应如何转换为无符号值,因为“标准没有定义有符号值应如何表示或在范围超出有符号类型时如何将无符号值转换为有符号值”。
然而,事实证明标准确实明确定义了从负有符号值转换为正无符号值的情况。对于整数而言,负有符号值x将被转换为UINT_MAX+1-x,就像它以二进制补码形式存储为有符号值,然后解释为无符号值一样。
因此,当你说:
unsigned char a;
unsigned char b;
unsigned int c;
a = 0;
b = -5;
c = a + b;
由于C标准将-5转换为一个无符号类型的值UCHAR_MAX-5+1(255-5+1),因此b的值变为251。然后进行加法运算。这使得a+b与0 + 251相同,然后存储在c中。但是,当你说:
unsigned char a;
unsigned char b;
unsigned int c;
a = 0;
b = 5;
c = (a-b);
printf("c dec: %d\n", c);
在这种情况下,a和b被提升为无符号整数,以匹配c,因此它们的值保持为0和5。然而,在无符号整数运算中,0-5会导致下溢错误,这被定义为结果为UINT_MAX+1-5。如果这发生在提升之前,那么值将是UCHAR_MAX+1-5(即251)。
然而,你在输出中看到-5的原因是因为无符号整数UINT_MAX-4和-5具有完全相同的二进制表示,就像单字节数据类型中的-5和251一样,而且当你使用“%d”作为格式化字符串时,printf会将c的值解释为有符号整数而不是无符号整数。
由于从无符号值到有符号值的转换对于无效值没有定义,结果变成了实现特定的。在你的情况下,由于底层机器对于有符号值使用二进制补码,所以无符号值UINT_MAX-4变成了有符号值-5。
第一个程序之所以不会发生这种情况,是因为无符号整数和有符号整数都可以表示251,因此在两者之间进行转换是明确定义的,并且使用“%d”或“%u”没有关系。然而,在第二个程序中,它会导致未定义的行为并变成实现特定,因为您的UINT_MAX-4的值超出了有符号整数的范围。
底层发生了什么
始终要仔细检查您认为正在发生或应该发生的事情与实际发生的事情,因此让我们现在从编译器中查看汇编语言输出,以确切地了解正在发生的情况。以下是第一个程序的有意义部分:
mov BYTE PTR [rbp-1], 0
mov BYTE PTR [rbp-2], -5
movzx edx, BYTE PTR [rbp-1]
movzx eax, BYTE PTR [rbp-2]
add eax, edx
请注意,尽管我们在字节b中存储了一个有符号值-5,但编译器在提升它时会通过零扩展数字来提升它,这意味着它被解释为11111011表示的无符号值而不是有符号值。然后将提升的值相加以成为c。这也是C标准定义有符号到无符号转换的方式--在使用二进制补码表示有符号值的体系结构上实现转换很容易。
现在看看程序2:
mov BYTE PTR [rbp-1], 0 ; a = 0
mov BYTE PTR [rbp-2], 5 ; b = 5
movzx edx, BYTE PTR [rbp-1] ; a is promoted to 32-bit integer with value 0
movzx eax, BYTE PTR [rbp-2] ; b is promoted to a 32-bit integer with value 5
mov ecx, edx
sub ecx, eax ; a - b is now done as 32-bit integers resulting in -5, which is '4294967291' when interpreted as unsigned
我们可以看到在进行任何算术运算之前,a和b再次被提升为无符号整数,因此我们最终会得到两个无符号整数的差,由于下溢导致结果为UINT_MAX-4,这也等同于有符号值的-5。因此,无论您将其解释为有符号还是无符号减法,由于机器使用二进制补码形式,结果都与C标准匹配,不需要任何额外的转换。
%d
格式说明符打印一个unsigned int
值,这是不合法的且行为未定义。为了有意义地在 printf 中输出一个无符号整数值,你需要使用%u
或任何其他预期unsigned int
参数的说明符。%x
是很好的选择,因为它预期unsigned int
类型。但%d
是完全不可接受的。这部分是你得到奇怪结果的原因之一。 - AnT stands with Russia