为什么 (1 >> 0x80000000) == 1?

14

数字1,右移任何大于0的位数,结果应该是0,对吗?但我可以输入这个非常简单的程序打印出1。

#include <stdio.h>

int main()
{
        int b = 0x80000000;
        int a = 1 >> b;
        printf("%d\n", a);
}

在 Linux 上使用 gcc 进行测试。


27
从技术上讲,这是未定义行为,因为您正在进行超过类型宽度的移位(gcc 应该会报告警告)。 - Matteo Italia
2
除非你进行强制类型转换,否则这不就是要求它向左移动1位吗? - Marvo
3
此外,似乎 gcc 完全尊重 UB 的精神,根据条件提供不同的“错误”结果(http://gcc.gnu.org/ml/gcc/2004-11/msg01131.html)。 - Matteo Italia
1
@MatteoItalia:在你的评论中,“Technically”这个词是什么意思?它削弱了(完全有效的)观点,即行为是未定义的。我不会期望gcc发出警告,除非您使用“-O”启用优化;没有数据流分析,编译器不知道“b”是否大于或等于“int”的宽度。 - Keith Thompson
1
@MatteoItalia:明白了。不幸的是,很多人用“技术上”一词来表示“标准规定是这样,但在实践中你无需担心它”。我不希望原帖作者如果没有看到警告就认为代码没问题。 - Keith Thompson
显示剩余4条评论
4个回答

30

6.5.7 位移运算符:

如果右操作数的值为负数或大于等于提升后的左操作数宽度,则行为未定义。

编译器可以做任何事情,但最常见的行为是完全优化表达式(以及依赖它的任何东西)或仅让底层硬件执行超出范围的移位。许多硬件平台(包括x86和ARM)掩盖一些低阶位用作移位量。实际的硬件指令将返回在这些平台上观察到的结果,因为移位量被掩盖为零。因此,在您的情况下,编译器可能已经优化了移位,或者只是让硬件执行其本身的操作。如果您想知道哪个,请检查汇编代码。


@MooingDuck:你不能从观察到的行为中推断出那个。还有其他几种可能的解释。 - Stephen Canon
我听说有些编译器通过仅使用 RHS 的最右边的五个位来实现这种行为,这解释了 OP 的行为。 - Mooing Duck
1
@da code monkey:为什么?not几乎肯定会和位移一样快,如果不是更快。 - BlueRaja - Danny Pflughoeft
如果你只是想在0和另一个你不关心的值之间切换,你可以使用~foo,它是按位取反。或者,如果你想在0和1之间切换,你可以使用foo^1。显然,这两种方法都只是在0和特定的另一个值之间切换。 - fluffy
是的,这个难题是当输入为0时返回1。 - Jeff Linahan
显示剩余2条评论

2
根据标准,超出实际位数的移位可能导致未定义的行为。因此我们不能责怪编译器。
这种做法的动机可能在于0x80000000的“边界含义”,该值处于最大正数和负数之间(并且是“负数”,具有最高位设置),以及应该执行的某些检查,编译程序不会执行以避免浪费时间验证“不可能”的事情(你真的想让处理器移动30亿次比特吗?)。

2
不是“可以导致”,而是“确实导致”。 - R.. GitHub STOP HELPING ICE
@R..:这取决于你所处的角度:从语言的角度来看,它“确实存在”(因为语言本身并没有说明任何内容),而从编译器的角度来看,它“可能存在”(因为编译器设计者可以“定义”它)。 - Emilio Garavaglia

1

很可能它并没有试图通过一些大量的位移来进行转换。

在您的系统上,INT_MAX 很可能是 2**31-1 或者 0x7fffffff(我使用 ** 表示指数)。如果是这种情况,在声明中:

int b = 0x80000000;

(在问题中缺少一个分号,请复制并粘贴您的确切代码) 常量0x80000000unsigned int类型,而不是int。该值被隐式转换为int。由于结果超出了int的限制,因此结果是实现定义的(或者在C99中可能会引发实现定义的信号,但我不知道任何实现这样做)。

这种情况下最常见的方法是将无符号值的位重新解释为2的补码有符号值。在这种情况下,结果是-2 ** 31,或-2147483648

因此,行为不是未定义的,因为您正在移位等于或超过int类型的宽度的值,而是未定义的,因为您正在移位(非常大的)负数值。

当然,这并不重要;未定义就是未定义。

注意:上述假设您的系统中的int为32位。如果int比32位更宽,则大部分内容不适用(但行为仍未定义)。
如果您真的想尝试按0x80000000位进行移位,可以像这样操作:
unsigned long b = 0x80000000;
unsigned long a = 1 >> b;    // *still* undefined

unsigned long被保证足够大以容纳值0x80000000,因此您避免了问题的一部分。

当然,移位的行为与原始代码中一样未定义,因为0x80000000大于或等于unsigned long 的宽度。 (除非您的编译器具有非常大的unsigned long类型,但实际上没有任何现实世界的编译器这样做。)

避免未定义行为的唯一方法是不要做您正在尝试做的事情。

原始代码的行为可能,但几乎不可能不是未定义的。这只有在从unsigned intint的实现定义转换产生介于0和31之间的值时才会发生。如果int小于32位,则转换很可能产生0。


最后一段并不是那么不可能发生的情况 - 在一个使用16位int的系统上,这种情况是相当可能的,而这种系统也并非闻所未闻。 - caf

0

阅读一下,或许能帮到你:

expression1 >> expression2

>> 运算符掩盖 expression2,以避免将 expression1 左移太多。这是因为如果移位量超过了 expression1 数据类型中的位数,所有原始位都将被移走,得到一个微不足道的结果。

现在为了确保每次移位至少留下一个原始位,移位运算符使用以下公式来计算实际移位量:

使用按位与运算符将 expression2 掩码(mask)为 expression1 中位数减一。

例如:

var x : byte = 15;
// A byte stores 8 bits.
// The bits stored in x are 00001111
var y : byte = x >> 10;
// Actual shift is 10 & (8-1) = 2
// The bits stored in y are 00000011
// The value of y is 3
print(y); // Prints 3

"8-1"是因为x是8个字节,所以操作将使用7位。那个void函数会删除原始链的最后一位。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接