在C语言中将整数除以2的幂

3

当试图用两种不同的方式将一个整数除以 2 的幂时,我得到两个不同的输出。 一种方法是通过将其向右移动 k 位来实现。另一种方法是将其除以 (1<<k)。

#include <stdio.h>

int main(void) {
  int y1 = 0x00061290;
  int y2 = 0xFFFE1A32;
  printf("(y1/(1<<k))=%x\n", (y1/(1<<5)));
  printf("(y2/(1<<k))=%x\n", (y2/(1<<5)));
  printf("(y1>>k))=%x\n", (y1>>5));
  printf("(y2>>k))=%x\n", (y2>>5));
  return 0;
}

输出:

(y1/(1<<k))=3094
(y2/(1<<k))=fffff0d2
(y1>>k))=3094
(y2>>k))=fffff0d1

当y1为0x00061290时,该整数的输出相同。当y2为0xFFFE1A32时,该整数的输出不同。

我猜是因为y1为正数,而y2不是。但我不确定。有人能告诉我它们在什么情况下不同,在什么情况下相同吗?如果可能的话,请解释两个操作的区别。谢谢

2个回答

4
你得到不同的输出结果是因为y1y2被定义为int而不是unsigned int。有符号除法向0取整(根据C99规范),而将负值右移会在你的平台上向负无穷大取整。C标准规定将负值右移的行为是实现定义的。
但请注意,将int传递给printf进行%x格式化具有未定义的行为。
如果你将y1y2定义为unsigned,那么除法和移位将产生相同的结果。
以下是修改后的版本:
#include <stdio.h>

int main() {
    unsigned y1 = 0x00061290;
    unsigned y2 = 0xFFFE1A32;
    printf("y1=%x, y1/(1<<k)=%x\n", y1, y1 / (1<<5));
    printf("y2=%x, y2/(1<<k)=%x\n", y2, y2 / (1<<5));
    printf("y1=%x, y1>>k=%x\n", y1, y1 >> 5);
    printf("y2=%x, y2>>k=%x\n", y2, y2 >> 5);
    printf("\n");
    int i1 = 511;
    int i2 = -511;
    printf("i1=%d, i1/(1<<k)=%d\n", i1, i1 / (1<<5));
    printf("i2=%d, i2/(1<<k)=%d\n", i2, i2 / (1<<5));
    printf("i1=%d, i1>>k=%d\n", i1, i1 >> 5);
    printf("i2=%d, i2>>k=%d\n", i2, i2 >> 5);
    return 0;
}

输出:

y1=61290, y1/(1<<k)=3094
y2=fffe1a32, y2/(1<<k)=7fff0d1
y1=61290, y1>>k=3094
y2=fffe1a32, y2>>k=7fff0d1

i1=511, i1/(1<<k)=15
i2=-511, i2/(1<<k)=-15
i1=511, i1>>k=15
i2=-511, i2>>k=-16

C99中已经修正了实现定义的舍入方向,现在已经被明确定义。请参见https://dev59.com/z3A65IYBdhLWcg3w1SbU。但是,OP可能正在编译为C89。 - Lundin
@Lundin:当然,“实现定义”仅适用于移位。我重新表述了答案以澄清这一点。 - chqrlie
也许我刚才读错了那句话。不管怎样,现在没问题了。对负数进行右移确实是有明确定义的。 - Lundin
@Lundin:你说得很有道理,这个短语的意思不够明确。"这种行为"并不够精确。 - chqrlie

0
通常,在许多汇编语言中,有两种不同的“右移”操作——按位右移(它将字面位移而不区分符号),以及逻辑右移,它将符号位“扩展”到左侧,因为旧位从右侧移除,以便保留符号并且其行为类似于(重复)除以2。
C语言的设计者们不想为不同情况使用不同的运算符(例如,对于按位右移和逻辑右移可能使用“>>”和“>>>”);并且也不想定义他们的“右移”实际行为,以便编写可移植代码的人可以编写快速代码(例如,如果CPU支持按位右移且程序员需要按位右移,则编写可移植代码的程序员不能指望“>>”给他们按位右移)。
相反,C语言的设计者将有符号整数的右移“实现定义”,因此它基本上是无用的。
您特定的编译器将“>>”视为按位右移(而不是逻辑右移)。这会导致不同的行为,因为符号未被保留。

即使在相同的CPU上,不同的编译器可能使用逻辑移位并产生相同的结果。

另一个问题是int y2 = 0xFFFE1A32;试图将一个“太大的正数”(大于INT_MAX)放入一个int中,编译器没有警告你,并且只是假装你的正数是一个负数。这是由C规范中无关的错误指导造成的 - 具体来说,整数溢出被定义为包装/截断(因此尝试将大象塞进鞋盒被认为是完全有效的,而不是一个简单检测到的错误)。


嗯... signed 整数溢出是 UB,尽管不完全确定是否也适用于分配太大的值。无论如何 QA 显然知道绕过 ('y1 is positive while y2 is not')... - Aconcagua
据我所知,对于隐式转换,有一套完全不同的规则(例如“通常算术转换”),并且忽略了(有符号的)溢出规则。虽然我不是语言律师,但我关心的是现实。实际情况是,你无法说服大多数(全部?)C编译器将其视为警告或错误(例如,使用-O3 -Wall -Wextra -Wstrict-overflow=5 -Warith-conversion -Wtype-limits -Wconversion -Wsign-conversion的GCC不会发出警告)。 - Brendan
确实 - 尽管问题的作者无论如何都知道这一点。如果她/他使用了明确的负文字面值,比如-0x1e5ce,那将更清楚明了 - 或者从一开始就使用更简单的数字,应该已经通过将-5向左移动一位来观察到了 ;) - Aconcagua
顺便说一句:“[...]因为符号没有保留。” 嗯,它是被保留的,QA想知道通过除法得到-15,但通过左移得到-16 - 两个结果都是负数... - Aconcagua

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