右移运算符的奇怪行为(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)。

我想原因是inttypes保存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)同样的事情