为什么(1 <> 31导致-1?
int z = 1; z <>= 31; printf ("%d\n",z);
当我运行代码时, z=-1
,为什么?
int z = 1; z <<= 31;
假设int
是32位且使用了二进制补码表示,左移位是C中的未定义行为,因为如果在int
类型中无法表示结果。 从标准:
E1 << E2的结果是E1左移E2位位置
...
如果E1具有带符号类型和非负值,并且E1×2 E2可在结果类型中表示,那么这就是结果值; 否则,行为未定义。
在实践中,它可能导致0x80000000
,其被视为负数。
负整数的右移是实现定义的行为:
E1 >> E2的结果是E1右移E2位的位置。
...
如果E1具有带符号类型和负值,则结果值是实现定义的。
在C ++中,左移是以类似的方式定义,直到C ++ 14 ,正如@TC所提到的(或者,有一些限制,甚至可能是直到C ++ 11 ,正如@MattMcNabb写的那样)。
但即使定义了左移并且0x8000000
是预期值,负数右移的结果仍然是实现定义的。
是的,移位有符号整数和负数是我相信的实现定义。
当你向右移动时,你的实现可能会进行符号位扩展。
因此,它不是从左侧移入零,而是在符号位中移位。 z <<= 31;
可能是将符号位设置为1,然后z >>= 31;
从左边0xFFFFFFFF
一个,所以最终得到一个0xFFFFFFFF
的位模式,在你的平台上被解释为值-1
(可能使用两个补码)。
假设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第1457期 ):
E1 << E2
的值是E1
左移E2
位位置; 空位是零填充的。 [...]否则,如果E1
具有有符号类型和非负值,并且E1×2
E2
可在结果类型的相应无符号类型中表示,则转换为结果类型的该值是结果值; 否则,行为未定义。
由于2 31在无符号的32位int
是可表示的,因此定义了行为,并将结果转换为( 31 ) int
; 此转换是根据§4.7[conv.integral] / p3实现定义的。 在使用二进制补码的典型系统中,你得到-2 31 ,在这种情况下,随后的右移也是实现定义的,因为值是负的。 如果执行算术移位,则符号位移入,最后为-1
。
假设你在谈论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
。 如果这是true
那么在某些人看来,这意味着整数溢出不再是未定义的,因此1 << 31
的结果必须是INT_MIN
。 有关此主题的进一步讨论, 请参阅此主题 。
假设我们得到这么多(即你的系统有32位整数, std::numeric_limits
,你可以与那些解释标准的人一起说明在签名的int溢出中没有UB在这种情况下)然后我们有以下内容:
assert( CHAR_BIT * sizeof(int) == 32 ); assert( std::numeric_limits::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
。
这是因为符号复制机制,当你将z移动31次时,1从第0位移位到第31位。 现在,你在第31位有1,这将被视为负数。 在负数中,使用符号复制机制,如果你右移负数,则保留符号位。 所以你在每个位的位置都是1,小数是-1。
如果你使用unsigned int
你会得到相同的结果。 问题是你已经使用<<
向左移31位加一位符号31次。 这是无符号行为,因为您已将左侧最重要的位丢失到符号位(这是由于未定义的行为而导致的结果)
当你进行右移时,当你有符号整数(你得到一个符号位到最高有效位的副本)时,这比你没有符号的那样(你从左侧得到一个零位)会有所不同。 通常,这意味着当你执行右移和右逻辑移位指令(相当于除以2但使用无符号数)时,你得到一个有符号整数的算术移位指令(相当于除以2)使用无符号数字进行右移。
只需尝试相同的声明z作为unsigned int z;
你会得到预期的行为。