无符号扩展和有符号扩展

12

有人能够解释一下以下代码的输出吗:

void myprint(unsigned long a)
{
    printf("Input is %lx\n", a);
}
int main()
{
    myprint(1 << 31);
    myprint(0x80000000);
}

使用 gcc main.c 命令可以输出:

Input is ffffffff80000000
Input is 80000000

为什么 (1 << 31) 被视为有符号的,而 0x80000000 则被视为无符号的?


ideone 上,我得到了两次 0x80000000 - Jabberwocky
7
你的代码在使用小于等于32位的int时会导致未定义行为。 - too honest for this site
3
@Olaf,你应该把这个作为问题的答案发布。 - Lundin
在你的 myprint 函数中使用 1UL 而不是 1。myprintfunction(1UL << 31);默认情况下,没有限定符的值被表示为整数。 - campescassiano
对于32位来说,是因为它会进入符号位,是吗? - user129393192
5个回答

15
在C语言中,表达式的结果取决于操作数(或部分操作数)的类型。特别地,1是一个int(有符号),因此1 << n也是int0x80000000的类型(包括符号)由规则here确定,并且它取决于您的系统上int和其他整数类型的大小,您尚未指定。选择一种类型,使得0x80000000(一个大的正数)在该类型的范围内。
如果您有任何误解:字面值0x80000000是一个大的正数。人们有时会错误地将其等同于负数,混淆值与表示。
在你的问题中,你说“为什么0x80000000被视为无符号数?”。然而,你的代码实际上并不依赖于0x80000000的有符号性。你对它唯一的操作是将其传递给接受unsigned long参数的函数。因此,它是有符号还是无符号并不重要;当传递给转换函数时,它将被转换为具有相同值的unsigned long。(由于0x80000000unsigned long的最小保证范围内,不存在超出范围的可能性)。
那么,这就解决了0x80000000的问题。那么1 << 31呢?如果您的系统具有32位int(或更窄),由于带符号算术溢出,这会导致未定义行为进一步阅读链接)。如果您的系统具有较大的int,则此代码将生成与0x80000000行相同的输出。
如果您改用1u << 31,并且您的系统具有32位int,则没有未定义行为,并且可以保证看到程序两次输出80000000

由于您的输出不是80000000,因此我们可以得出结论,您的系统具有32位(或更窄)int,并且您的程序实际上会导致未定义的行为。如果int是32位,则0x80000000的类型将为unsigned int,否则为unsigned long


0x80000000的符号确实很重要,如果需要修改清除第30位比特的代码,如flags &= ~0x0000000040000000;或清除第32位比特的代码flags &= ~0x0000000100000000;,则可能会出现问题。在任一情况下,最明显的逻辑修改flags &= ~0x0000000080000000;在32位系统上无法正常工作,尽管该代码可以正常处理其他两个值。 - supercat
@supercat,我的回答是针对OP发布的代码。0x80000000被转换为unsigned long,没有进行任何其他操作。 - M.M
如果0x80000000被提升为有符号长整型,程序的输出将会不同。 - supercat
@supercat 这里没有涉及到任何推广。我猜你的意思是“如果0x80000000有类型为'signed long'...”,然而程序的输出在那种情况下也是相同的。 - M.M
@M.M,您能否确认我的答案(如下所示)是否正确。我认为0x80000000首先被转换为无符号数,然后在作为参数传递给myprint函数时被转换为无符号长整型。 - Saksham Jain
@SakshamJain 在你的系统上,0x80000000 已经是 unsigned int。它并没有被“转换为无符号”。当你调用函数时,unsigned int 会从 unsigned int 转换为 unsigned long,但这不会改变它的值。 - M.M

6
为什么将(1 << 31)视为有符号数,而0x80000000视为无符号数?
从C11规范的6.5.7位移运算符中可以得知:
整数提升会对每个操作数进行处理。结果的类型是提升后左操作数的类型。如果E1具有无符号类型,则结果值为E1 × 2的E2次方,取模比结果类型能表示的最大值多1。如果E1具有有符号类型和非负值,并且E1 × 2的E2次方能够在结果类型中表示,则该值就是结果;否则,行为是未定义的。
因此,由于1是int类型(在下面段落中提到的第6.4.4.1节),所以对于1 << 31,在int类型上也是未定义的,对于int小于或等于32位的系统可能甚至会陷入异常状态。
在6.4.4.1中提到的整数常量中:
十进制常量以非零数字开头,由一系列十进制数字组成。八进制常量由前缀0和仅由数字0到7的序列(可选)组成。十六进制常量由前缀0x或0X和由十进制数字和字母a(或A)到f(或F)组成,分别表示10到15的值。
并且
整数常量的类型是其值能够表示的相应列表中的第一个类型。
后缀 | 十进制常量 | 十六进制常量 ---------+------------------------------------+--------------------------- none | int | int | int | unsigned int | | long int | long int | unsigned long int | | long long int | long long int | unsigned long long int ---------+------------------------------------+--------------------------- u或U | unsigned int | unsigned int [...] | [...] | [...]
因此,0x80000000被视为无符号数。

因此,在具有32位或更少位int和32位或更大的unsigned int的系统上,0x80000000是一个无符号整数。


“Not well defined”是委婉语。它可能会导致鼻音守护进程出现。 - too honest for this site
你是对的@Olaf。在写这篇文章时,我不确定signed int型溢出的结果是未指定还是未定义的。我只知道它是“未定义的”,并且可能会陷入困境(实际上,在某些系统上会陷入困境)。此外,我的答案侧重于类型而不是。经过进一步咨询,我相信它是未定义的。 - Mohit Jain
请翻译以下有关编程的内容,从英文到中文。只返回翻译后的文本: https://dev59.com/02Qn5IYBdhLWcg3wto5W. 不知道哪些编译器实际采用这种方法。但gcc有一个选项(ftrapv)可启用陷阱机制。 - Mohit Jain
68K也有trapv指令,但那是关于机器的,与C无关。gcc选项更有趣(刚刚忘了),但我不确定它是否真正支持所有架构。有些可能甚至不会标记溢出,因此您必须提前检测它,需要大量代码。 - too honest for this site
1
有些CPU具有饱和加/减功能,但主要用于DSP算法。 - too honest for this site
显示剩余2条评论

2
您显然使用的是32位intunsigned int系统。 1适合于int,因此它是一个signed int0x80000000不适合。对于十进制常量,将使用下一个更大的有符号类型来保存该值,而对于十六进制和八进制常量,则首先使用相应的无符号类型(如果适用)。这是因为它们通常是无符号的。有关完整的值/类型矩阵,请参见C标准,6.4.4.1p5
对于有符号整数,左移并改变符号是未定义行为。这意味着所有的赌注都已经失去了,因为您超出了语言规范。
话虽如此,以下是结果的解释:
  • 在您的系统上,long 显然是64位的。
  • int1 移位到了符号位,正如您所预期的那样。
  • 这导致了一个负的 int
  • 负的 int 会被转换为无符号的 unsigned,这样2的补码表示就不需要任何操作(只需要重新解释位模式)。
  • 由于使用了64位的 unsigned long,因此符号被扩展到上位,成为 myprint 的参数。

如何避免这个问题:
  • 在移位操作时,始终使用无符号整数(例如,在适当的情况下向整数常量添加 U 后缀,如这里的 1U 或者 0x1U)。
  • 当使用比 int 更小的类型时,请注意标准整数转换。
  • 通常情况下,如果您需要一个特定的大小,则应该使用 stdint.h 固定宽度类型。请注意,标准整数类型没有定义的位宽。对于 32 位,可以使用 uint32_t 来声明变量。对于常量,请使用宏: UINT32_C(1)(不带后缀!)。

1
我的想法是:第一个调用'myprint()'的参数是一个表达式,因此必须在运行时计算。因此,编译器需要通过生成的指令将其解释为有符号的int左移,产生一个负的有符号int,然后进行符号扩展以填充long,然后重新解释为无符号的long。(我认为这可能是编译器错误?)
相比之下,第二个调用'myprint()'是一个硬编码的整数常量表达式,传递给一个以unsigned long作为参数的例程;我认为编译器是根据这个上下文假定常量表达式已经是unsigned long,因为没有明显的冲突类型信息。

0

如果我错了,请纠正我。这就是我的理解。

在我的机器上,如M.M所说,sizeof(int) = 4。 (通过打印sizeof(int)确认)

因此,1 << 31变成(有符号的)0x80000000,因为1是有符号的。 但是,0x8000000变为无符号,因为它不能适合有符号int(因为它被视为正数,并且int的最大正数可以是0x7fffffff)。

因此,当有符号int转换为long时,会发生符号扩展(使用符号位进行扩展)。而当无符号int转换时,则使用0进行扩展。

这就是为什么在myprint(1 << 31)的情况下会有额外的1,而在以下情况下不是:

1)myprint(1u << 31)

2)myprint(1 << 31),当int> 32位时,因为在这种情况下,符号位不是1。


1 << 31 是未定义行为,您不能依赖或推断在此之后发生的任何事情。我不愿意在这里进一步猜测,因为人们会读到它并想到自己“它实际上不是未定义的,真正的故事是bla bla bla,我可以依靠它”。 - M.M
@M.M 谢谢。我刚刚明白了。早些时候应该读你提供的链接http://stackoverflow.com/questions/26319592/1-31-produces-the-error-the-result-of-the-expression-is-undefined。 非常感谢! - Saksham Jain
@M.M 我怀疑我没有。如果 E1 × 2^E2 在结果类型中无法表示,则 E1 << E2 的结果是未定义的。这是否意味着当编译器左移一位以乘以2时,它正在冒险可能会出现未定义的行为?(溢出仍然是良好定义的行为,对吗?) - Saksham Jain
有符号溢出是未定义的。左移并乘以2可能会由于溢出而导致未定义行为。 - M.M

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