在C语言中,字面量和变量有什么区别(signed vs unsigned short ints)?

8
我在《计算机系统:程序员的视角,第二版》一书中看到了以下代码。该代码正常工作并创建所需的输出。输出可以通过有符号和无符号表示的差异来解释。
#include<stdio.h>
int main() {
    if (-1 < 0u) {
        printf("-1 < 0u\n");
    }
    else {
        printf("-1 >= 0u\n");
    }
    return 0;
}

上述代码生成-1 >= 0u,然而下面的代码应该是相同的,但实际上并不相同!换句话说,
#include <stdio.h>

int main() {

    unsigned short u = 0u;
    short x = -1;
    if (x < u)
        printf("-1 < 0u\n");
    else
        printf("-1 >= 0u\n");
    return 0;
}

产生了 -1 < 0u 的结果,这是为什么呢?我无法解释这个问题。
请注意,我看到过类似的问题,例如这个问题,但它们并没有帮助。
PS。正如@Abhineet所说,将short改为int可以解决这个困境。然而,如何解释这个现象呢?换句话说,在4字节中,-10xff ff ff ff,在2字节中是0xff ff。以二进制补码的形式给出,它们分别被解释为unsigned,它们具有相应的值429496729565535。它们都不小于0,我认为在两种情况下,输出都需要-1 >= 0u,即x >= u
在小端Intel系统上的示例输出:
对于短整型:
-1 < 0u
u =
 00 00
x =
 ff ff

对于int类型:

-1 >= 0u
u =
 00 00 00 00
x =
 ff ff ff ff

1
将无符号类型转换为有符号类型(int,char)的方法是什么? - Lundin
4
在C语言中,重要的是数值本身,而不是它们的表示方式。关于二进制补码、十六进制的FFFF和65535等方面的内容都是无关紧要的。 - M.M
不要对非代码文本使用代码格式。 - user207421
4个回答

10
上面的代码产生了 -1 >= 0u。
所有的整型字面值(数值常量)都有一个类型,因此也有一个符号。默认情况下,它们是有符号的 int 类型。当你附加 u 后缀时,你将字面值转换为 unsigned int
对于任何 C 表达式,其中你有一个操作数是有符号的,另一个是无符号的,平衡规则(正式地说:通常的算术转换)会隐式地将有符号类型转换为无符号类型。
从有符号类型到无符号类型的转换是明确定义的(6.3.1.3):

否则,如果新类型是无符号的,则通过反复添加或减去比新类型中可以表示的最大值多一的值,直到该值在新类型的范围内。

例如,在标准的二进制补码系统中,对于32位整数,无符号整数的最大值为2^32 - 1(4294967295,在limits.h中表示为UINT_MAX)。最大值加一即为2^32。而-1 + 2^32 = 4294967295,所以字面量-1被转换为值为4294967295的无符号整数。这个值比0大。
当你将类型切换为short时,你最终得到的是一个小整数类型。这就是两个示例之间的区别。每当小整数类型是表达式的一部分时,整数提升规则会将其隐式转换为更大的int(6.3.1.1):
如果int可以表示原始类型的所有值(对于位域,由宽度限制),则该值将转换为int;否则,它将转换为unsigned int。这些被称为整数提升。所有其他类型都不受整数提升的影响。
如果在给定平台上short比int小(在32位和64位系统上是这种情况),任何short或unsigned short因此总是会被转换为int,因为它们可以放在其中一个中。
因此,对于表达式if(x

谢谢。这解释了这种情况。不过,我想知道为什么设计师们会这样决定?你有什么想法吗? - Ali Shakiba
2
当你遇到不同的操作数时,你可以决定在进行比较之前将两个操作数都转换为“有符号”或“无符号”。由于“有符号整数”是“默认”类型,因此在一个操作数中使用“无符号”字面量意味着你有理由指定这个附加限定词。而且,由于C语言被设计成“接近硬件”,所以自然而然地尝试将所有内容适应于平台的本机字长,以便能够使用适当的指令(这些指令不会“混合”操作数类型,并且通常对字长操作数进行操作)。 - vgru
2
@AliShakiba 整数提升背后最初的理由是:如果你有例如 char x = 200, char y=200; 然后执行 x + y,那么表达式不会溢出。然而,整数提升是 C 语言中的类型不一致,多年来它带来的伤害远大于好处,因为隐式提升错误比简单的整数溢出错误更难以发现。此外,隐式类型提升规则有些复杂,因此有很多 C 程序员不知道它们的工作原理,这是不幸的。 - Lundin
@Lundin:最初的理由还指出,大多数实现会将短无符号类型的操作提升为有符号类型,并以一种与提升为无符号类型无法区分的方式进行处理,即使结果在INT_MAX+1uUINT_MAX的范围内,除非结果在某些情况下被使用,这几乎肯定影响了将事物提升为有符号的决定,因为在大多数现有实现中,有符号提升通常是正确的,但在差异会有所影响的情况下,无符号提升是正确的... - supercat
在标准没有要求的情况下,但现有实现做得正确的情况下,这种情况就会出现。我怀疑标准的作者们会写出他们所写的规则,如果他们预计编译器针对静默环绕平台的代码有时会像uint1 = ushort1*ushort2;一样在范围INT_MAX+1uUINT_MAX内以奇怪的方式处理。 - supercat

3

你遇到了C语言的整数提升规则。

小于int类型的运算符会自动将操作数提升为intunsigned int。详细解释请参考评论。如果在此之后二元(两个操作数)运算符的类型仍不匹配(例如unsigned int和int),则还有进一步的步骤。我不会试图总结更详细的规则。请参阅Lundin的答案

这篇博客文章更详细地介绍了这一点,并提供了与您类似的示例:signed和unsigned char。它引用了C99规范:

如果int可以表示原始类型的所有值,则将该值转换为int;否则,它将被转换为unsigned int。这些被称为整数提升。所有其他类型都不会受到整数提升的影响。


您可以在像godbolt这样的平台上更轻松地进行实验,使用返回一或零的函数。只需查看编译器输出即可了解最终发生的情况。

#define mytype short

int main() {
    unsigned mytype u = 0u;
    mytype x = -1;
    return (x < u);
}

这是一个很好的观点。然而,正如问题末尾的示例所显示的那样,shortunsigned short各自占用两个字节,但解释不同。感谢提供链接。 - Ali Shakiba
@AliShakiba:整数提升的规则可能不太直观。这就是为什么使用 short 变量(都会提升为 int)与 int 变量(int 无法表示所有可能的无符号整数)会得到不同结果的原因。 - Peter Cordes
3
这里实际上有两组规则: "整型提升" 将 shortunsigned short(在此平台上)都提升为 int,而 "通常的算术转换" 将 intunsigned 进行比较时,将 int 提升为 unsigned。大多数运算符会执行 "整型提升",然后执行 "通常的算术转换"。 唯一的例外是位移运算符,它们只执行整型提升。 - Dietrich Epp
这是一个很好的观点。在C语言中,对于unsigned类型的扩展规则是在左侧填充额外的新位数为0,而对于signed类型则为msb。因此,有两种情况:(1)如果我们首先进行扩展,然后转换为无符号并最终进行比较,则0xff ff将被扩展为0xff ff ff ff,然后作为无符号数与0进行比较,导致-1 >= 0u。(2)如果我们首先将其转换为无符号数,然后进行扩展,0xff ff将被扩展为0x 00 00 ff ff,最后将其与0u进行比较,我认为应该评估为-1 >= 0u。在这两种情况下,都需要是-1 >= 0u!我完全困惑了! - Ali Shakiba
1
你的回答表明参数是由于给>不同类型而被提升。然而,这是错误的。如果参数是两个shorts,则两者仍然都会被提升为int。整数提升首先发生在>和大多数其他二元运算符中:小于int的类型将被提升为int。只有在类型仍然不同的情况下,才需要进一步的转换。 - M.M
@M.M和Dietrich:感谢您们的纠正。我更新了我的答案,尽量不说错任何话。 - Peter Cordes

2
与您认为的不同,这不是特定宽度(此处为2字节与4字节)的属性,而是应用规则的问题。整数提升规则表明,在相应值范围适合于int的所有平台上,short和unsigned short将转换为int类型。由于这种情况在这里存在,两个值都被保留并获得了int类型。-1在int中可以完美表示,0也是如此。因此,测试结果为-1小于0。
在测试-1与0u的情况下,公共转换选择无符号类型作为它们转换为的公共类型。将-1转换为 unsigned 后得到的值是 UINT_MAX,该值大于 0u。
这是一个很好的例子,说明为什么你永远不应该使用“狭窄”的类型进行算术或比较。只有在有严格的大小限制时才使用它们,对于简单变量来说这很少出现,但对于大型数组来说可能有所帮助。

在数组中存储窄类型的数据非常好。但是,将数组值加载到窄局部变量中通常不是一个好主意。将数组值加载到“int”局部变量中可以避免担心整数提升规则;只需遵循有符号与无符号int的通常规则即可。至少x86具有高效的指令,可以在从内存加载到寄存器时动态地对int8_t或int16_t进行符号扩展。我不知道ARM或其他重要架构的情况。 - Peter Cordes

0

0u 不是 unsigned short,而是 unsigned int

编辑:行为的解释, 如何执行比较?

由Jens Gustedt回答,

这被标准称为“通常算术转换”,并且适用于同一运算符的两个不同整数类型出现时。

本质上它做了什么

如果类型具有不同的宽度(更精确地说是标准所称的转换等级),则将其转换为更宽的类型,如果两种类型具有相同的宽度,则除了真正奇怪的架构之外,它们中的无符号类型获胜。将值-1从带符号转换为无符号类型始终导致无符号类型的最高可表示值。

他撰写的更详细的博客可以在此处找到。


谢谢@Abhineet。没错。但是,我很好奇为什么会发生这种情况?在4字节中的-10xff ff ff ff,而在2字节中是0xff ff。将它们作为2s补码解释为“无符号数”,它们具有相应的值429496729565535。它们都不小于0,我认为在两种情况下,输出都需要是-1 >= 0u,即x >= u - Ali Shakiba
1
这并没有回答问题,也没有解释为什么“int”变量与“short”变量给出不同的结果。 - Peter Cordes

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