int z = 1;
z <<= 31;
z >>= 31;
printf ("%d\n",z);
当我运行代码z=-1
时,为什么?
答案 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
较大,则此代码定义明确,导致z
为1
。
在C和C ++中,z <<= 31
被定义为z = z << 31
。
在C11中,<<
被解释为(6.5.7 / 4):
E1 << E2
的结果是E1
左移E2
位位置;腾出的位充满了 零。 [...]如果E1
具有签名类型和非负值,并且E1
×2
E2
在结果类型中可表示,那就是结果价值;否则,行为未定义。
在这种情况下,E1
为z
,1
,E2
为31
。但是,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,您将获得预期的行为。