右移运算符的奇怪行为(1 >> 32)
我最近面对使用右移运算符的奇怪行为。
以下程序:
#include <cstdio> #include <cstdlib> #include <iostream> #include <stdint.h> int foo(int a, int b) { return a >> b; } int bar(uint64_t a, int b) { return a >> b; } int main(int argc, char** argv) { std::cout << "foo(1, 32): " << foo(1, 32) << std::endl; std::cout << "bar(1, 32): " << bar(1, 32) << std::endl; std::cout << "1 >> 32: " << (1 >> 32) << std::endl; //warning here std::cout << "(int)1 >> (int)32: " << ((int)1 >> (int)32) << std::endl; //warning here return EXIT_SUCCESS; }
输出:
foo(1, 32): 1 // Should be 0 (but I guess I'm missing something) bar(1, 32): 0 1 >> 32: 0 (int)1 >> (int)32: 0
foo()
函数会发生什么? 我知道它和最后两行之间的唯一区别是最后两行是在编译时计算的。 如果我使用64位整数,为什么它“工作”?
任何关于此的灯将不胜感激!
肯定有关,这是g++
给的东西:
> g++ -o test test.cpp test.cpp: In function 'int main(int, char**)': test.cpp:20:36: warning: right shift count >= width of type test.cpp:21:56: warning: right shift count >= width of type
这可能是CPU实际上在计算
a >> (b % 32)
在foo
; 同时,1 >> 32是一个常量expression式,所以编译器会在编译时折叠这个常量,以某种方式给出0。
由于标准(C ++ 98§5.8/ 1)指出
如果右操作数是负数,或者大于或等于升级的左操作数的位数,则行为是不确定的 。
foo(1,32)
和1>>32
给出了不同的结果。
另一方面,你在bar
提供了一个64位的无符号值,因为64> 32保证结果必须是 1/2 32 = 0。然而,如果你写
bar(1, 64);
你仍然可以得到1。
编辑:在x86 / x86-64(Intel#253667,第4-404页a >> (b % 32/64)
上,逻辑右移(SHR)的行为类似于a >> (b % 32/64)
32/64):
目标操作数可以是寄存器或内存位置。 计数操作数可以是立即数或CL寄存器。 计数器被屏蔽到5位(如果是64位模式,则使用6位,使用REX.W)。 计数范围限制为0到31(如果使用64位模式和REX.W,则为63)。 提供了一个特殊的操作码编码,计数为1。
但是,在ARM(至lessarmv6&7)上,逻辑右移(LSR)实现为(ARMISA页面A2-6)
(bits(N), bit) LSR_C(bits(N) x, integer shift) assert shift > 0; extended_x = ZeroExtend(x, shift+N); result = extended_x<shift+N-1:shift>; carry_out = extended_x<shift-1>; return (result, carry_out);
哪里(ARMISA Page AppxB-13)
ZeroExtend(x,i) = Replicate('0', i-Len(x)) : x
这保证了≥32的右移将产生零。 例如,当这个代码在iPhone上运行时, foo(1,32)
将会给出0。
这些显示将32位整数移位≥32是不可移植的。
好。 所以它在5.8.1:
操作数应为整数或枚举types,并执行整体升级。 结果的types是升级的左操作数的types。 如果右操作数是负数,或者大于或等于升级的左操作数的位数,则行为是不确定的。
所以你有一个未定义的行为(tm)。
foo会发生什么,移位宽度大于或等于被移位的数据的大小。 在C99标准中导致未定义的行为。 无论使用什么C ++标准的MS VC ++,它可能都是一样的。
其原因是为了让编译器devise人员能够利用任何CPU硬件支持进行轮class。 例如,i386架构有一个将32位字移位若干位的指令,但位数是在5位宽的指令字段中定义的。 最有可能的是,您的编译器正在生成指令,通过获取您的位移量并用0x1F屏蔽它来获取指令中的位移。 这意味着移位32与移位0相同。
我使用VC9编译器在32位窗口上编译它。 它给了我下面的警告 。 由于sizeof(int)
在我的系统上是4个字节,编译器指出右移32位会导致未定义的行为。 由于它是未定义的,所以不能预测结果。 只是为了检查我右移31位,所有的警告消失,结果也如预期(即0)。
我想原因是int
types保存32位(对于大多数系统),但是一个位用于符号,因为它是有符号types。 所以只有31位用于实际值。
警告说这一切!
但公平地说,我曾经被同样的错误所困扰。
int a = 1; cout << ( a >> 32);
是完全不确定的。 事实上,编译器通常会给出与我的经验不同的结果。 我的意思是,如果编译器可以在运行时看到移位expression式的值,那么它可能会给运行时的expression式赋予不同的结果。
foo(1,32)执行一个旋转的狗屎,所以应该消失在右边的位重新出现在左边。 如果你这样做了32次,那么设置为1的单个位就回到原来的位置。
bar(1,32)是相同的,但是该位在64-32 + 1 = 33位,这是32位int的可表示数字之上。 只有32个最低位被采用,而且都是0。
1 >> 32由编译器执行。 不知道为什么gcc在这里使用非循环移位而不是在生成的代码中。
((int)1 >>(int)32)同样的事情