您看到的是算术移位,与您期望的按位移位相反;即,编译器不是“粗暴地”移动位,而是传播符号位,从而除以 2 N。
当谈到unsigned int
s 和正int
s 时,右移是一种非常简单的操作——无论它们的含义如何,这些位都会向右移动一个位置(在左侧插入 0)。在这种情况下,该操作相当于除以 2 N(实际上 C 标准是这样定义的)。
在谈论负数时会出现区别。存在几种负数表示,尽管目前对于整数最常用的是2 的补码表示。
对于初学者来说,这里“残酷”的按位移位的问题是,其中一个位以某种方式用于表示符号;因此,无论负整数表示如何,移动二进制数字都会产生意想不到的结果。
例如,通常在 2 的表示中,最高有效位是 1 表示负数,0 表示正数;对负数应用按位移位(在左侧插入零)将(在其他事物之间)使其为正数,而不导致(通常预期的)除以 2 N
因此,引入了算术移位;用 2 的补码表示的负数有一个有趣的特性:如果不是从左侧插入零,而是插入与原始符号位具有相同值的位,则可以保留移位的除以 2 N行为。
通过这种方式,只需在移位中增加一点额外的逻辑,就可以执行 2 N的有符号除法,而无需求助于成熟的除法例程。
现在,是否保证有符号整数的算术移位?在某些语言中是1,但在 C 中并非如此 - 移位运算符在处理负整数时的行为被保留为实现定义的细节。
通常情况下,这是由于对操作的硬件支持不同;C 用在非常不同的平台上,特别是在过去,根据平台的不同,操作的“成本”存在很大差异。
例如,如果处理器不提供算术右移指令,编译器将被要求发出某种慢得多的DIV
指令,这可能是较慢处理器的内部循环中的问题。由于这些原因,C 标准让实现者为当前平台做最合适的事情。
在您的情况下,您的实现可能选择了算术移位,因为您在 x86 处理器上运行,该处理器使用 2 的补码算术并提供按位和算术移位作为单 CPU 指令。
- 实际上,像 Java 这样的语言甚至具有分离的算术和位移运算符——这主要是因为它们没有
unsigned
类型来存储位域。