您所看到的是底层机器如何表示数字的结果,C 标准如何定义有符号到无符号类型转换(用于算术)以及底层机器如何表示数字(用于未定义行为的结果)结尾)。
当我最初写我的回复时,我假设 C 标准没有明确定义应如何将有符号值转换为无符号值,因为该标准没有定义应如何表示有符号值或如何将无符号值转换为有符号值当范围超出有符号类型的范围时。
然而,事实证明,当从负符号转换为正无符号值时,标准确实明确定义了这一点。在整数的情况下,负符号值 x 将被转换为 UINT_MAX+1-x,就像它被存储为二进制补码中的有符号值然后解释为无符号值一样。
所以当你说:
unsigned char a;
unsigned char b;
unsigned int c;
a = 0;
b = -5;
c = a + b;
b 的值变为 251,因为使用 C 标准将 -5 转换为无符号类型的值 UCHAR_MAX-5+1 (255-5+1)。然后在转换之后进行添加。这使得 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 ; a becomes 0
mov BYTE PTR [rbp-2], -5 ; b becomes -5, which as an unsigned char is also 251
movzx edx, BYTE PTR [rbp-1] ; promote a by zero-extending to an unsigned int, which is now 0
movzx eax, BYTE PTR [rbp-2] ; promote b by zero-extending to an unsigned int which is now 251
add eax, edx ; add a and b, that is, 0 and 251
请注意,虽然我们在字节 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 标准匹配,无需任何额外转换。