移位有符号整数和短整型时符号扩展的不一致性

3
int main(){
  signed int a = 0b00000000001111111111111111111111; 
  signed int b = (a << 10) >> 10;
  // b is: 0b11111111111111111111111111111111

  signed short c = 0b0000000000111111; 
  signed short d = (c << 10) >> 10;
  // d is: 0b111111

  return 0;
}

假设 int 是 32 位,short 是 16 位,
为什么 b 会被符号扩展,而 d 不会被符号扩展呢?我已经在 x64 上使用 gcc 编译器并使用 gdb 进行了测试。
为了让 short 被符号扩展,我不得不像这样使用两个独立的变量:
  signed short f = c << 10;
  signed short g = f >> 10;
  // g is: 0b1111111111111111

1
只是猜测,但可能优化器意识到 signed short d = (c << 10) >> 10; 没有可观察的行为。但使用两个变量意味着存在一个序列点,那么就有了可观察的行为? - Jerry Jeremiah
这是一个 MVCE:https://godbolt.org/z/hzGaW6Pro - Jerry Jeremiah
1
如果你打印 sizeof(c<<10),它会打印 int 类型的字节数,因此这实际上是整数提升。 - Jerry Jeremiah
请注意,二进制字面量目前(截至2020年2月草案标准)尚未成为标准C的一部分(但它们是标准C++的一部分)。使用0b0001类型符号表示法是使用标准的扩展,虽然被广泛支持但不是标准。 - Jonathan Leffler
3个回答

6
signed short的情况下,当一个小于int的整数类型用于表达式中时,它(在大多数情况下)被提升为int类型。这在C标准的第6.3.1.1p2节中详细说明:
以下内容可以在表达式中使用intunsigned int类型的地方使用:
  • 具有整数类型(除了intunsigned int)的对象或表达式,其整数转换级别小于或等于intunsigned int的级别。
  • _Boolintsigned intunsigned int类型的位字段。
如果int能够表示原始类型的所有值(按位字段的宽度限制),则将该值转换为int;否则,它将转换为unsigned int。 这些称为整数提升。所有其他类型都不受整数提升的影响。
并且这种提升特定发生在按位移位运算符的情况下,如第6.5.7p3节所述:
对每个操作数执行整数提升。结果的类型是提升后左操作数的类型。如果右操作数的值为负数或大于或等于提升后左操作数的宽度,则行为未定义。
因此,short值0x003f被提升为int值0x0000003f,并进行左移位运算。 这将导致0x0000fc00,右移位运算的结果为0x0000003f。 signed int情况稍微有些有趣。 在这种情况下,您将一个带有值1的位左移进符号位。 根据6.5.7p4,这触发未定义行为

E1 << E2 的结果是将 E1 左移 E2 位;空余的位将填上零。如果 E1 是无符号类型,则结果的值为 E1×2E2,对于能在结果类型中表示的最大值再加一后对其取模。如果 E1 是带符号类型且非负值,并且 E1×2E2 在结果类型中可表示,则结果为该值;否则,行为未定义。

因此,虽然您得到的 signed int 的输出可能符合您的预期,但实际上它是未定义的行为,因此您不能依赖该结果。


像int64这样的大值呢?它们也保持不变吗? - Dan
@Dan 这些数据类型的级别比 int 更高,因此不会进行提升。 - dbush
故事的寓意是:不要对有符号整数类型应用移位操作。这些陷阱是MISRA规则明确禁止的原因 - 在旧的MISRA-2004标准中,这是规则12.7。 - DavidHoadley

2
short会通过整数提升(integer promotions)自动转换为int,根据C 2018 6.5.7 3的规定:

对于每个操作数都执行整数提升…

因此,(c << 10)将一个int0b111111左移10位,得到(在您的C实现中)32位的int 0b00000000000000001111110000000000。该值的符号位为零;它是一个正数。
当您执行signed short f = c << 10;时,c << 10的结果太大,无法适应signed short。它是64,512,超过了您的signed short可以表示的最大值32,767。在赋值中,该值将被转换为左操作数的类型。根据C 2018 6.3.1.3 3的规定,转换是由实现定义的。GCC定义此转换为模65536取余(wrap modulo)。因此,将64,512转换为短整型会得到64,512 - 65,536 = -1024。所以f设置为-1024。
然后,在f >> 10中,您正在移位一个负值。作为signed shortf仍然被提升为int,但这种转换会保留该值,导致一个int值为-1024。然后进行移位。此移位由实现定义,GCC定义它为带符号扩展的移位。因此,-1024 >> 10的结果为-1。

但我要求移位一个“short”值,为什么它会将其提升为int?我如何强制实现与int相似的行为? - Dan
1
在这里解释了 - 查找“整数提升”。(链接:https://dev59.com/z1YO5IYBdhLWcg3wRvgh#46073296) - anatolyg
2
@Dan:你不能要求C语言去移动一个“short”值。在移位表达式中,以及大多数C表达式中,“short”操作数会自动转换为“int”。在C语言中没有办法表示对“short”值的移位。(在一个C实现中,“short”和“int”具有相同的宽度,你可以得到相同的结果,尽管从技术上讲,移位仍然是在“int”值上完成的。) - Eric Postpischil
像 int64 这样的更大值怎么办?它们保持不变吗? - Dan

0

首先根据C标准(6.5.7位移运算符)

3 对每个操作数执行整数提升。结果的类型是提升后的左操作数的类型。

因此这个值

signed short c = 0b0000000000111111;

在此声明中使用的表达式
signed short d = (c << 10) >> 10;

被提升为整数类型int。由于该值为正,因此提升后的值也为正。

因此,这个操作

c << 10

不涉及符号位。

另一方面,此代码片段

signed int a = 0b00000000001111111111111111111111; 
signed int b = (a << 10) >> 10;

由于根据C标准的同一部分,E1 << E2存在未定义行为。

4 E1 << E2 的结果是将E1左移E2位;空出的位用零填充。如果E1具有无符号类型,则结果的值为E1×2E2,对结果类型可表示的最大值加1取模。如果E1具有带符号类型和非负值,并且E1×2E2在结果类型中可表示,则该值为结果;否则,行为未定义。


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