这是两个非常简单的程序。我希望得到相同的输出,但我没有。我无法弄清楚为什么。第一个输出251.第二个输出-5。我可以理解为什么251.但是,我不明白为什么第二个程序给我一个-5。
#include <stdio.h>
int main()
{
unsigned char a;
unsigned char b;
unsigned int c;
a = 0;
b= -5;
c = (a + b);
printf("c hex: %x\n", c);
printf("c dec: %d\n",c);
}
输出:
c hex: fb
c dec: 251
#include <stdio.h>
int main()
{
unsigned char a;
unsigned char b;
unsigned int c;
a = 0;
b= 5;
c = (a - b);
printf("c hex: %x\n", c);
printf("c dec: %d\n",c);
}
输出:
c hex: fffffffb
c dec: -5
答案 0 :(得分:12)
在第一个程序中,b=-5;
将251分配给b
。 (转换为无符号类型总是减少模1的值加上目标类型的最大值。)
在第二个程序中,b=5;
只需将{5}分配给b
,然后c = (a - b);
执行减法0-5 作为类型int
到期默认促销 - 简单地说,“小于int
”类型总是被提升为int
,然后才被用作算术和按位运算符的操作数。
编辑:我错过了一件事:由于c
的类型为unsigned int
,因此第二个程序中的结果-5将转换为unsigned int
已执行c
分配,结果为UINT_MAX-4
。这是您使用%x
说明符printf
看到的内容。使用c
打印%d
时,会得到未定义的行为,因为%d
需要一个(签名的)int
参数,并且您传递的unsigned int
参数的值为在普通(签名)int
中无法表示。
答案 1 :(得分:2)
您正在使用格式说明符%d
。将参数视为带符号的十进制数(基本上为int
)。
您从第一个程序获得251,因为(unsigned char)-5
是251,然后您将其打印为带符号的十进制数字。它被提升为4个字节而不是1个,这些位是0
,因此数字看起来像0000...251
(其中251
是二进制的,我只是没有转换它)。
你从第二个程序得到-5,因为(unsigned int)-5
是一个很大的值,但是已经转换为int
,它是-5
。由于您使用printf
的方式,它被视为一个int。
使用格式说明符%ud
打印无符号的十进制值。
答案 2 :(得分:2)
这里有两个不同的问题。第一个事实是,对于看起来相同的操作,您将获得不同的十六进制值。您缺少的基本事实是char
被提升为int
s(与short
s一样)来进行算术运算。区别在于:
a = 0 //0x00
b = -5 //0xfb
c = (int)a + (int)b
此处,a
扩展为0x00000000
,b
扩展为0x000000fb
(不符号扩展,因为它是 unsigned char)。然后,执行添加,我们得到0x000000fb
。
a = 0 //0x00
b = 5 //0x05
c = (int)a - (int)b
此处,a
已扩展为0x00000000
,b
已扩展为0x00000005
。然后,执行减法,我们得到0xfffffffb
。
解决方案?坚持char
或int
s;混合它们会导致你不会期望的事情。
第二个问题是unsigned int
正在打印为-5
,显然是签名值。但是,在字符串中,您告诉printf
打印其第二个参数,解释为signed int(这是"%d"
的含义)。这里的诀窍是printf
不知道你传入的变量的类型。它只是按字符串告诉它的方式解释它们。这是一个示例,我们告诉printf
将指针打印为int:
int main()
{
int a = 0;
int *p = &a;
printf("%d\n", p);
}
当我运行这个程序时,我每次都得到一个不同的值,即a
的内存位置,转换为基数10.您可能会注意到这种情况会引发警告。你应该阅读编译器给你的所有警告,只有在你完全确定你正在做你想做的事情时才会忽略它们。
答案 3 :(得分:1)
您所看到的是底层机器如何表示数字的结果 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,因为-5使用C标准转换为无符号类型的值UCHAR_MAX-5 + 1(255-5 + 1)。然后在转换之后进行添加。这使得+ 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。
在第一个程序中没有发生这种情况的唯一原因是因为unsigned int和signed int都可以表示251,所以两者之间的转换是明确定义的,使用“%d”或“%u”不会物。但是,在第二个程序中,由于UINT_MAX-4的值超出了signed int的范围,因此会导致未定义的行为并变为特定于实现。
幕后发生了什么
最好仔细检查一下你认为发生了什么或者实际发生了什么应该发生,所以现在让我们看看编译器的汇编语言输出,看看究竟发生了什么。这是第一个项目中有意义的部分:
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标准相匹配,无需任何额外转换。
答案 4 :(得分:-1)
为无符号变量分配负数基本上违反了规则。你正在做的是将负数转换为大的正数。从技术上讲,你甚至不能保证从一个处理器到另一个处理器的转换是相同的 - 在1的补码系统上(如果仍然存在),你将获得不同的值,例如。
所以你得到了你得到的。你不能指望签名代数仍然适用。