【问题标题】:Why does right shifting negative numbers in C bring 1 on the left-most bits? [duplicate]为什么在 C 中右移负数会使最左边的位变为 1? [复制]
【发布时间】:2013-06-25 20:34:37
【问题描述】:

Herbert Schildt 的《C The Complete Reference》一书说:“(在有符号负整数的情况下,右移将导致带入 1,从而保留符号位。)”

保留符号位有什么意义?

此外,我认为这本书指的是使用符号位而不是使用two's complement 表示负数的情况。但即使在那种情况下,推理似乎也没有任何意义。

【问题讨论】:

  • "保留符号位的意义何在?" -- 因为 -2/2 是 -1。 “我认为这本书指的是使用符号位而不是使用 2s 补码表示负数的情况。” ——不。“即使在那种情况下,推理似乎也没有任何意义。” -- 不,你缺乏理解力。
  • @Lundin:如果该答案已过时,那么可以说您应该在此处发布更好的答案(或更新已接受的答案)。无论哪种方式,它都不会阻止这个 Q 是重复的......
  • @JonathanLeffler C 标准允许除二进制补码之外的其他形式的符号。在不使用二进制补码的 CPU 上,我并不清楚算术移位在实践中的含义。为什么它不能使用旋转? C 标准中没有任何内容禁止它。

标签: c bit-manipulation bit-shift


【解决方案1】:

Schildt 的书被广泛认为是非常糟糕的。

事实上,C 保证当你右移一个负符号数时会移入一个 1。负值右移的结果是实现定义的。

然而,如果一个负数的右移被定义为以 1s 移动到最高位的位置,那么在 2s 补码表示上,它将表现为 算术移位 em> - 右移 N 的结果将与除以 2N 的结果相同,向负无穷舍入。

【讨论】:

  • 你能解释一下“右移 N 的结果将与除以 2^N 相同,向负无穷舍入。”吗?
  • @JagsVG:例如,如果您有 8 位 2s 补码二进制数 11111101 表示十进制中的 -3,并且您执行算术右移以给出 11111110 表示十进制中的 -2,这是与将 -3 除以 2^1 相同,得到 -1.5 向负无穷大舍入,得到 -2。
【解决方案2】:

与 Schildt 先生的许多声明一样,该声明是全面且不准确的。许多人建议把他的书扔掉。 (在其他地方,请参阅 The Annotated Annotated C StandardACCU Reviews — 在 Schildt 上进行作者搜索;另请参阅 Stack Overflow 上的 Definitive List of C Books

实现定义是否右移一个负数(必须有符号)整数将零或一移到高位。底层 CPU(例如,ARM;另请参见 class)通常有两个不同的底层指令——ASR 或算术右移和 LSR 或逻辑右移,其中 ASR 保留符号位而 LSR 不保留。编译器编写者可以选择其中之一,并且可能出于兼容性、速度或奇思妙想的原因这样做。

ISO/IEC 9899:2011 §6.5.7 位移位运算符

¶5 E1 >> E2 的结果是 E1 右移 E2 位位置。如果E1 具有无符号类型 或者如果E1 具有带符号类型和非负值,则结果的值是整数 E1 / 2E2 商的一部分。如果E1 具有带符号类型和负值,则 结果值是实现定义的。

【讨论】:

【解决方案3】:

关键是 C >>(右移)运算符保留1(已签名)int 的符号。

例如:

int main() {
  int a;
  unsigned int b;

  a = -8;
  printf("%d (0x%X) >> 1 = %d (0x%X)\n", a, a, a>>1, a>>1);

  b = 0xFFEEDDCC;
  printf("%d (0x%X) >> 1 = %d (0x%X)\n", b, b, b>>1, b>>1);

  return 0;
}

输出:

-8 (0xFFFFFFF8) >> 1 = -4 (0xFFFFFFFC)                    [sign preserved, LSB=1]
-1122868 (0xFFEEDDCC) >> 1 = 2146922214 (0x7FF76EE6)      [MSB = 0]

如果它不保留符号,结果将毫无意义。你会取一个小的负数,然后右移一位(除以二),你最终会得到一个大的正数。

1 - 这是实现定义的,但根据我的经验,大多数编译器选择算术(符号保留)移位指令。

【讨论】:

  • 在这里,我想我有一个简单的问题要回答一次...快速手指的方法!
  • 不;这是实现定义的。
  • @NikunjBanka “但是为什么它会保留这个标志?这有什么具体原因吗?” -- 刚刚给你解释过了。
  • @NikunjBanka 可能是因为int 是有符号类型。如果您不想要这种行为,请使用unsigned type
  • @OliCharlesworth:虽然它在技术上是由实现定义的,但我希望-1>>1 != -1 的实现比(unsigned char)(-1) != 0xFF 的实现更罕见。如果一个人想要的是 99% 的编译器将为 x>>1 实现的行为,我知道没有更好或更清晰的表达方式。
【解决方案4】:

在有符号负整数的情况下,右移将导致带入 1,从而保留符号位

不一定。参见 C 标准 C11 6.5.7:

E1 >> E2 的结果是 E1 右移 E2 位位置。如果 E1 有 一个无符号类型,或者如果 E1 有一个有符号类型和一个非负值, 结果的值是 E1 / 商的整数部分 2E2如果 E1 有带符号类型和负值,则结果值 是实现定义的。

这意味着编译器可以随意转换它喜欢的任何值(0 或 1),只要它记录它。

【讨论】:

  • 我在CHAR_BITS != 8 使用的编译器比-1 >> 1 != -1 使用的多。你遇到过不复制符号位的编译器吗?
  • @supercat 我不记得有任何这样的编译器,根据我的经验,大多数编译器对有符号数使用算术右移而不是逻辑右移。但是在 99% 的情况下,对有符号类型使用位运算符没有多大意义。
  • 给定四个有符号数 a、b、c、d,使得 |a+b+c+d| (a+b+c+d+2)>>2 比我能想到的任何替代方案都更清晰、更快捷。如果存在下限除法运算符,则可能更可取,但没有。使用带符号操作数的除法运算符没有意义,因为编译器必须做额外的工作来以不同的方式处理负值,然后用户代码必须做额外的工作才能撤消它。
  • 我很好奇截断除法是如何成为常态的;我记得在 FORTRAN 的最早版本中,整数除法的行为仅针对正操作数定义,并且从“不为不使用的东西付费”的角度来看,这对 C 也是有意义的。如果在很多情况下可以使用截断结果而不考虑操作数的符号(因为取底结果可以在上面的平均值中),这可能是有道理的,但这种情况似乎很少见。如果代码需要关心操作数的符号...
  • ...它通常可以确保除法操作数始终为正数。例如,(total < 0 ? (signed)((1u-total) / 4u)-1 : (signed)((total+2u)/4u) 可能与使用除法运算符的任何其他方法一样有效,但它永远不需要除负值。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-11-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-12-23
相关资源
最近更新 更多