为什么(1 << 31) >> 31的结果是-1?

3
int z = 1;
z <<= 31;
z >>= 31;
printf ("%d\n",z);

当我运行代码时,为什么z=-1

1
你正在看到一个算术右移,但这是实现定义的。 - Paul R
@PaulR 这是实现定义的,因为在不使用二进制补码的架构上,结果可能会有所不同。除了一些非常奇特的实现之外,所有实现都使用您期望的语义来实现带符号右移。 - fuz
@FUZxxl:我认为这并不是那么简单,例如,如果一个特定的二进制补码CPU缺少ASR指令,那么使用LSR进行>>可能更有意义。 - Paul R
@PaulR 的意思是,除了一些非常古老的版本之外,它们中没有一个缺少这个指令。 - fuz
1
@PaulR 很好的观点。我在谈论托管系统。 - fuz
显示剩余3条评论
6个回答

9
int z = 1;
z <<= 31;

假设 int 是32位的并且使用二进制补码表示,左移在C语言中是未定义的行为,因为结果如果不能用int类型来表示。 标准如下:

E1 << E2 的结果是将E1向左移动E2个位置

...

如果E1具有带符号类型和非负值,并且E1 × 2E2可以在结果类型中表示,则该值为结果;否则,行为未定义。

实际上,它很可能会导致0x80000000,这被视为负数。
而对于负数的右移是实现定义的行为: E1 >> E2 的结果是将 E1 右移 E2 位。如果 E1 具有带符号类型并且具有负值,则结果值是实现定义的。
C++中,左移运算符的定义方式与C++14之前类似,正如@T.C.所提到的(或者,在一些限制下,可能甚至是C++11,正如@MattMcNabb所写的那样)。
但是,即使左移运算符已经定义并且期望值为0x8000000,负数的右移结果仍然是实现定义的。

你引用的是哪个标准和哪个版本? - T.C.
@T.C.:看起来像是C99。C++11几乎相同(只是为了匹配C++术语而指定“值”而不是“结果”)。 - Mike Seymour
更糟糕的是:一个知道你从一个正数开始的 C++ 编译器可以将 x>>31 优化为 x=0。同样,它可以将 x<<31 视为证明 x<=0(以及 x>=-1 可能?不确定),因为其他任何行为都会导致未定义的行为。 - Yakk - Adam Nevraumont

6

没错,对于带符号整数和负数的右移操作,具体实现是由编译器决定的。

你使用的实现可能会在右移时进行符号位扩展。

因此,它不是从左边移入零,而是移入符号位。z <<= 31; 可能将符号位设置为1,然后 z >>= 31; 从左侧移入1,这样你最终得到的位模式是 0xFFFFFFFF,在你的平台上被解释为值为 -1(该平台可能使用二进制补码)。


1
右移对于带符号的 负数 是实现定义的。 - AlexD
谢谢,我明白了。 - thephoquang

2
假设使用32位的int,在C11和C++11中这是未定义行为,但在C++14中是实现定义的。
C11 §6.5.7/p4(引用N1570):
"The result of E1 << E2 is E1 left-shifted E2 bit positions; vacated bits are filled with zeros. [...] If E1 has a signed type and nonnegative value, and E1 × 2E2 is representable in the result type, then that is the resulting value; otherwise, the behavior is undefined."
C++11规则在N3337 §5.8 [expr.shift]/p2几乎相同。由于2的31次方通常无法表示为带符号32位int,因此行为未定义。
C++14 §5.8 [expr.shift]/p2(引用N3936;另见CWG issue 1457):
"The value of E1 << E2 is E1 left-shifted E2 bit positions; vacated bits are zero-filled. [...] Otherwise, if E1 has a signed type and non-negative value, and E1×2E2is representable in the corresponding unsigned type of the result type, then that value, converted to the result type, is the resulting value; otherwise, the behavior is undefined."
由于2的31次方可以表示为无符号32位int,因此行为是定义的,并且结果是2的31次方转换为(有符号)int;根据§4.7 [conv.integral]/p3,此转换是实现定义的。在使用二进制补码的典型系统中,您将得到-2的31次方,此后右移也是实现定义的,因为该值为负数。如果执行算术移位,则符号位被移入,最终得到-1

0

这是因为符号复制机制,每当您将 z 左移 31 次时,1 就从第 0 位位置移到第 31 位位置。现在您在第 31 位上有 1,这将被视为负数。在负数中使用符号复制机制,即如果您对负数进行右移,则会保留符号位。所以,在每个位位置上,您都有 1,这在十进制中为 -1。


0

如果您使用的是unsigned int,则会得到相同的结果。问题在于您使用了<<将31位加上一个符号位左移31次。这是无符号行为,因为您已经失去了左侧最高位进入符号位(这就是您得到未定义行为结果的原因)

当您进行右移时,当您有带符号整数时,这是以不同方式进行的(您将符号位复制到最高位),而当您有无符号整数时,则从左侧获取零位移位。通常,这意味着当您使用带符号整数进行右移时,您会得到一个右算术移位指令(相当于除以二),而当您使用无符号数字进行右移时,您会得到一个右逻辑移位指令(也相当于除以二,但使用无符号数字)。

只需将z声明为unsigned int z;,您将获得预期的行为。


0

假设您现在谈论的是int为32位或更小的情况。(如果int更大,则此代码定义良好,并导致z1)。

在C和C++中,z <<= 31被定义为z = z << 31

在C11中,<<解释为(6.5.7/4):

E1 << E2的结果是将E1左移E2个位位置;空出的位以零填充。[...] 如果E1具有有符号类型并且非负值,并且E1×2E2可以用结果类型表示,则这就是结果值;否则,行为未定义。

在这种情况下,E1z,即 1,而 E231。然而,231 在一个最大值为 231-1 的32位 int 中是不可表示的,因此行为未定义
当发生未定义的行为时,任何事情都可能发生,从看到意外输出,到程序崩溃,再到导弹发射等等。
在 C99、C++98 和 C++03 中,<< 具有类似的定义;对于32位或更小的整数,1 << 31 在所有这些情况下都是未定义的行为。
在 C++11 和 C++14 中有一个选项可以测试,std::numeric_limits<int>::is_modulo。如果这是true,则在某些人的看法中,这意味着整数溢出不再是未定义的,因此 1 << 31 的结果必须是 INT_MIN。有关此主题的进一步讨论,请参见此线程

假设我们走到了这一步(即您的系统具有32位整数,且std::numeric_limits<int>::is_modulo == true,并且您支持那些解释标准的人表示在这种情况下有没有符号整数溢出的 UB),则我们有以下内容:

assert( CHAR_BIT * sizeof(int) == 32 );
assert( std::numeric_limits<int>::is_modulo == true );
int z = 1;
z <<= 31;
assert(z == INT_MIN);

现在讨论z >>= 31。这被定义为z = z >> 31;。在C++11 5.8/3中:

E1 >> E2的值是E1向右移动E2位。[...]如果E1具有带符号类型和负值,则结果值是实现定义的。

由于INT_MIN是一个负值,行为是实现定义的。这意味着您的实现必须记录它的操作,因此您可以查阅编译器的文档以了解此处发生了什么。

一个可能的解释是它执行算术移位,这意味着位向右移动,但符号位保留其值。这意味着您最终得到所有位都是1,这在二进制补码中是-1


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