为什么(1 <&lt; 31)&gt;&gt; 31导致-1?

时间:2014-10-04 11:18:38

标签: c++ c bit-shift

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

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

6 个答案:

答案 0 :(得分:9)

int z = 1;
z <<= 31;

假设int是32位且使用two's complement表示,左移是 C 中的未定义行为,因为如果int中无法表示结果类型。从标准:

  

E1&lt;&lt;&lt; E2 E1 左移 E2 位位置

     

...

     

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

在实践中,它可能会导致0x80000000,这被视为负数。

整数的右移是实现定义的行为:

  

E1&gt;&gt;的结果E2 E1 右移 E2 位位置。

     

...

     

如果 E1 具有签名类型和负值,则结果值是实现定义的。


C ++ 中左移定义以类似的方式定义,直到 C ++ 14 ,如@T.C.所述(或者,有一些限制,可能是偶数直到 C ++ 11 ,正如@MattMcNabb写的那样。)

但即使定义了左移并且0x8000000是预期值,负数右移的结果仍然是实现定义的。

答案 1 :(得分:5)

是的,移位有符号整数和负数是我认为的实现定义。

当您向右移动时,您的实现可能会进行符号位扩展。

因此,它不是从左侧移入零,而是在符号位中移位。 z <<= 31;可能将符号位设置为1,然后z >>= 31;从左侧移位,因此最终得到0xFFFFFFFF的位模式,其被解释为值{{1在您的平台上(可能使用两个补码)。

答案 2 :(得分:2)

假设32位int,这是C11和C ++ 11中的未定义行为,但在C ++ 14中定义了实现。

C11§6.5.7/ p4(引用N1570):

  

E1 << E2的结果是E1左移E2位位置;   腾出的位用零填充。 [...]如果E1有签名类型   和非负值,E1 × 2 E2 可表示   结果类型,那就是结果值;否则,   行为未定义。

N3337§5.8[expr.shift] / p2中的C ++ 11规则几乎完全相同。由于2 31 通常不能在带符号的32位int中表示,因此行为未定义。

C ++14§5.8[expr.shift] / p2(引用N3936;另见CWG issue 1457):

  

E1 << E2的值为E1左移E2位位置;   空位是零填充的。 [...]否则,如果E1已签名   类型和非负值,E1×2 E2 是可表示的   在结果类型的相应无符号类型中,然后   转换为结果类型的值是结果值;   否则,行为未定义。

由于2 31 在无符号32位int中可表示,因此定义了行为,结果是2 31 转换为(signed){ {1}};此转换是根据§4.7[conv.integral] / p3实现定义的。在使用二进制补码的典型系统中,你得到-2 31 ,在这种情况下,随后的右移也是实现定义的,因为该值是负的。如果执行算术移位,则符号位移入,最后得到int

答案 3 :(得分:0)

假设您在这里谈论的int是32位或更小。 (如果int较大,则此代码定义明确,导致z1

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

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

  

E1 << E2的结果是E1左移E2位位置;腾出的位充满了   零。 [...]如果E1具有签名类型和非负值,并且E1×2 E2 在结果类型中可表示,那就是结果价值;否则,行为未定义。

在这种情况下,E1z1E231。但是,2 31 在32位int中无法表示,其最大值为2 31 - 1,因此行为未定义

当发生未定义的行为时,可能会发生任何事情,包括您看到意外输出,程序崩溃,发射导弹等等。

在C99中,C ++ 98和C ++ 03 <<具有相似的定义; 1 << 31在所有这些行为中都是未定义的行为(对于32位或更小的整数)。


在C ++ 11和C ++ 14中,您可以选择std::numeric_limits<int>::is_modulo进行测试。如果这是true那么在某些人看来,这意味着整数溢出不再是未定义的,因此1 << 31的结果必须是INT_MIN。有关此主题的进一步讨论see this thread

假设我们到目前为止(即你的系统有32位整数和std::numeric_limits<int>::is_modulo == true,并且你支持那些解释标准的人,说在这种情况下没有对符号int溢出的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的值&gt;&gt; E2是E1右移E2位位置。 [...]如果E1具有有符号类型和负值,则结果值是实现定义的。

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

可能的解释是它执行arithmetic shift,这意味着位向右移位,但符号位保留其值。这意味着你最终得到了所有位 - 一位,即-1的二进制补码。

答案 4 :(得分:0)

这是因为符号复制机制,当您将z移位31次时,1从第0位移位到第31位。现在,你在第31位有1,这将被视为负数。并且在负数中,使用符号复制机制,其中如果右移负数,则保留符号位。所以你在每个位的位置都是1,小数点是-1。

答案 5 :(得分:0)

如果您使用unsigned int,您将获得相同的结果。问题是你使用<<向左移31位加一位符号31次。这是无符号行为,因为您已将左侧最重要的位丢失到符号位(这是由于未定义的行为而导致的结果)

当你进行右移时,当你有符号整数(你得到一个符号位的副本到最高有效位)时,这比你没有符号的那样(你从左边有一个零位移位)会有所不同侧)。通常,这意味着当你执行右移和右逻辑移位指令(相当于除以2,但是使用无符号数)时,你得到有符号整数的算术移位指令(相当于除以2)使用无符号数字进行右移。

尝试使用与unsigned int z;相同的声明z,您将获得预期的行为。