硬件如何知道一个变量是正数还是负数?

5

如果这个问题太基础了,我很抱歉...我在任何地方都没有找到答案。

假设我像这样声明一个C变量:

unsigned int var = 241;

在这种情况下,变量是无符号的,因此我的意图是它应具有小数值241。

或者我可以这样声明:

signed int var = -15;

在这个例子中,我将其声明为有符号整数,因此按照我的理解它应该具有小数值-15。

然而两次,我假设变量将在内存(硬件)中声明为这样: 1111 0001。

那么处理器如何知道,在最低级别即硬件上,我打算将其声明为241还是-15呢? 我知道使用二进制补码表示负数等,但是我假设在硬件中,处理器只看到一系列的1和0,然后通过切换一些IC的状态执行某些操作。 处理器如何知道是否要将位序列解释为标准二进制(用于无符号)或2的补码(用于有符号)?

还有另一个稍微不相关的问题:

  1. 在C语言中,我可以这样做:

    unsigned int var = -15; printf("The var is: %d ", var); 这将像预期的那样打印-15。 为什么当我这样做时:

signed int var = 0xF1; //或0b11110001 printf("The var is: %d ", var);

我得到了241而不是-15?既然我将其声明为有符号的,并且在二进制补码中0xF1是-15,为什么我得到等于标准二进制中的0xF1的值241?

  1. 为什么编译器让我这样做:

unsigned int var = -15;

它不应该抛出错误告诉我不能将负值赋给我声明为无符号的变量吗?

谢谢您,对于我的许多也许基本的问题,我表示歉意,我有很多不知道的东西:D。


1
为什么编译器会允许我这样做:unsigned int var = -15;?因为C语言有规则,可以将超出范围(负数或大数)的整数隐式转换为无符号类型:模数归约到值范围内。(在2的补码机器上,这意味着对于运行时变量使用带符号的比特模式,但不适用于1的补码或符号/幅度C实现。) - Peter Cordes
“处理器如何知道”它不知道。处理器只知道内存中存储的位数值。但编译器可能会根据您将变量声明为有符号还是无符号而生成不同的指令(通常会这样做)。 - dxiv
@dxiv 抱歉,在你的评论出现时我已经在编辑了。希望你能接受我的答案并不是基于你非常相似的贡献。 - Yunnosch
1
@Yunnosch 当然,我的只是一个摘要评论,而你的是一个正式的答案(+1)。 - dxiv
2个回答

8
硬件并不知道。
编译器知道。
编译器之所以知道,是因为你在这里说了 signed int var = -15;,“亲爱的编译器,这是一个可以是负数的变量,我把它初始化为一个负值。”
而在这里你又说了另一种方式 unsigned int var = 241;,“亲爱的编译器,这是一个不能是负数的变量,我把它初始化为一个正值。”
编译器会将这些信息牢记在心,并针对变量及其值执行相应的代码,生成一组机器语言指令,进而使硬件按照预期行事。因此,硬件最终做出的行为是适当的,无论是负数还是非负数都是如此;这并不是因为它知道,而是因为没有选择。
有趣的一点是,“相应的指令”(正如Peter Cordes在下面的评论中指出的)对于负值的2补码表示这个特殊但广泛使用的情况来说,实际上是相同的(这是2补码的重要优势)。

2
2的补码之美在于加法/减法与无符号数相同,非扩展乘法也是如此。只有一补或符号/幅度机器需要不同的指令来执行有符号/无符号基本数学运算。(2的补码机器只需要不同的比较和/或分支指令)。 - Peter Cordes
@PeterCordes 确实。我认为我的回答没有与此相矛盾。如果有的话,请随时指出来。也许是“那组指令...相应地”?但这并不一定意味着“负/正或有符号/无符号不同”。最终可能只涉及到例如printf()中的内容。 - Yunnosch
1
我现在明确提到了它,因为我自己是2补数属性的粉丝。@PeterCordes - Yunnosch
好的,更新不错。不想让人们误解普通计算机有一个“signed-add”指令。只有像“x < y”这样的东西取决于有符号还是无符号,这当然不是罕见的,但不像“x += y”那么常见。 - Peter Cordes
ISO C并没有明确规定表示负数的方案应该是什么,只规定了表示的下限。这与许多较新的语言(例如Java)形成对比。有时这很重要。 - Neil
1
@Neil:如果我没记错的话,除了限制之外,ISO C 还在某个地方说过,三个选项是符号/幅度、反码和补码。(在对象表示细节中的某个地方,这些细节限制了当您使用 unsigned char* 访问其他类型的对象表示时会看到什么。例如,无符号整数类型必须是二进制的,但可以有填充。而有符号整数可以有填充和一个符号位。) - Peter Cordes

5
如果这两个值是 char 类型(有符号或无符号),那么它们在内存或寄存器中的内部表示(8位模式)将相同。 唯一的区别在于编译器处理这些值时生成的指令不同。 例如,如果这些值存储在在 C 中声明为 signedunsigned 的变量中,则对这些值进行比较会使编译器在汇编级别生成一个特定于 signedunsigned 的比较指令。
但在你的例子中,你使用的是 int 类型。 假设在你的平台上,这些 int 使用了四个字节,那么你给出的这两个常量在其 32 位模式方面是不相同的。 高位考虑到值的符号并向上填充 0 或 1 到 32 位(参见下面的 0f 序列)。
请注意,将负值分配给 unsigned int 在编译时会产生警告,如果使用适当的编译器标志(例如 -Wconversion)。 在下面的评论中,@PeterCordes 提醒我们,这样的赋值在 C 中是合法的,并且在某些情况下很有用;使用(或不使用)编译器标志来检测这种情况只是个人选择的问题。 但是,将 -15U 赋值给变量而不是 -15,可以明确表明将该常量视为无符号的意图(尽管带有负号),并且不会触发警告。
int i1=-15;
int i2=0xF1;
int i3=241;
printf("%.8x %d\n", i1, i1); // fffffff1 -15
printf("%.8x %d\n", i2, i2); // 000000f1 241
printf("%.8x %d\n", i3, i3); // 000000f1 241
unsigned int u1=-15; // warning: unsigned conversion from ‘int’ to ‘unsigned int’ changes value from ‘-15’ to ‘4294967281’
unsigned int u2=0xF1;
unsigned int u3=241;
printf("%.8x %u\n", u1, u1); // fffffff1 4294967281
printf("%.8x %u\n", u2, u2); // 000000f1 241
printf("%.8x %u\n", u3, u3); // 000000f1 241

2
我猜那个转换警告是来自MSVC的?GCC和clang不会发出警告,因为它是完全合法的C语言,而-16U-1U是一些位模式的有用方式,例如x&-16U向下舍入到16的倍数。所以在这种情况下收到警告会很烦人。但是MSVC的-Wall启用了一堆警告,包括那些经常是虚假阳性的警告,所以这很好并且可能有用,并且非常适合这种情况。(我希望GCC或clang在-Wpedantic或其他地方有一个警告,但我没有找到。) - Peter Cordes
@PeterCordes 是的,我总是使用许多-W... -pedantic标志来检测这些潜在错误(尽管在您提供的一些情况下被认为是正确的)。 我更喜欢在这些转换是正确的情况下使用强制转换来消除编译器警告,但这只是个人选择。 - prog-fh
2
请注意,在正式的C术语中,“-15U”从未是负数。正如MSVC所警告的那样(https://godbolt.org/z/nf9x8M):“* C4146:对无符号类型应用一元减运算符,结果仍为无符号*”。因此,它与“(0U - 15U)”完全相同。 C数字文字不包括减号;这就是为什么在32位int C实现中,“-0x80000000”具有类型“unsigned”的原因:0x80000000不适合有符号的32位int,因此升级为无符号,然后应用一元“-”。https://godbolt.org/z/eW7bsx。这意味着您通常需要将其转换回有符号或类似的内容来定义“INT_MIN”。 - Peter Cordes

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