在C语言中无法将负数向右移位

3

我正在学习《C语言程序设计》(作者为K&R)。目前我正在学习位运算部分,但是我对以下代码理解困难。

int mask = ~0 >> n;

我打算使用这个来掩盖另一个二进制数字的左侧,就像这样。 0000 1111 1010 0101 // 随机数

我的问题是,当我打印变量mask时,它仍为负数-1。假设n为4。我以为将~0(即-1)向左移动会得到15(0000 1111)。

感谢回答。


这本书声誉很高,但它非常老旧,在处理现代(2005年后)的C编译器时会让你误入歧途。 - zwol
@zwol,你推荐哪本书? - TheCaptain
@TheCaptain 很抱歉,我没有关于一本较新的书的推荐。我自己很久以前学过C语言。 - zwol
3个回答

6
对负数进行右移操作将产生一个实现定义的值。大多数托管实现都会在左侧移入1位,就像您所看到的那样,但这并不一定是必须的。无符号类型以及有符号类型的正值在向右移位时始终在左侧移入0位。因此,您可以通过使用无符号值来获得所需的行为:
unsigned int mask = ~0u >> n;

这种行为在C标准的第6.5.7节中有记录:
5. E1 >> E2 的结果是 E1 右移 E2 位。如果E1具有无符号类型或E1具有带符号类型且非负值,则结果的值是 E1 / 2E2 商的整数部分。如果E1具有带符号类型和负值,则结果的值是实现定义的。

1
更精确地说,它产生一个实现定义的值。N1570 6.5.7p5。(实现定义的行为可能允许其执行产生值以外的其他操作。) - Keith Thompson
实现定义值是不可预测的值? - TheCaptain
@TheCaptain 实现定义是指C标准没有规定值必须是什么,但具体的实现(如gcc、MSVC等)必须记录在该情况下它所做的事情。如果这些值是不可预测的,那么就称为不定值或未指定值。 - dbush

3

将负数有符号整型右移是一种实现定义的行为,通常(但不总是)会用1填充左侧而不是0。这就是为什么无论您向右移动多少位,结果始终为-1,因为左侧始终由1填充。

当您移动无符号整数时,左侧始终会被0填充。所以您可以这样做:

unsigned int mask = ~0U >> n;
                      ^

请注意,int通常为2或4字节,这意味着如果您想获得15,您需要向右移动12或28位而不仅仅是4位。您可以使用char代替:

unsigned char mask = ~0U;
mask >>= 4;

U代表什么?我在哪里可以阅读更多关于这个话题的资料?这本书没有提供很好的细节。根本就没有U。 - TheCaptain
@TheCaptain:整数常量上的 U 后缀表示它是一个无符号类型,在这种情况下特指 unsigned int - Keith Thompson
@TheCaptain 我认为C++ Reference 是一个很好的参考网站。查看一下那个链接,你会找到有关移位运算符的详细描述。 - user9522315
1
将负有符号整数向右移位会产生一个实现定义的结果。 - Keith Thompson
@KeithThompson 我意识到了。看看现在的编辑是否有意义。 - user9522315
请注意,使用32位的intn==0时,int mask = ~0U >> n;会导致将一个超出范围的unsigned赋值给int,这样会产生更多的实现定义行为。使用无符号类型进行移位操作要简单得多。 - chux - Reinstate Monica

2
在C语言和许多其他语言中,>> (通常) 用于对有符号变量(如int)进行位移时是一种算术右移。这意味着从左侧移入的新位是前一个最高有效位(MSB)的副本。这具有保留二进制补码负数的符号(以及值)的效果。
与此相反的是逻辑右移, 在逻辑右移中,MSB总是替换为零位。当变量是无符号类型 (例如unsigned int)时,会应用这种方法。
来自维基百科的描述:

C和C ++中的>>运算符不一定是算术移位。仅当其在有符号整数类型的左侧使用时,它通常才是算术移位。如果它代替使用了无符号整数类型,则它将是一个逻辑移位。

在你的情况下,如果你计划在位级别上工作(即使用掩码等),我强烈建议做两件事:
  1. 使用无符号值。
  2. 使用来自 <stdint.h> 的具有特定大小的类型,例如 uint32_t

你的修改现在有意义了 :-) - iBug
1
将负有符号整数向右移位会产生一个实现定义的结果。 - Keith Thompson

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