为什么 abs(0x80000000) == 0x80000000?

22

我刚开始阅读《黑客的乐趣》,它将abs(-231)定义为-231。为什么呢?

我在几个不同的系统上尝试了printf("%x", abs(0x80000000)),结果都是返回0x80000000。


2
阅读《Hacker's Delight》得到+1。 - Paul R
@Paul 谢谢!我才刚刚完成第一章。 - sigjuice
读完书后,请访问以下网站了解更多相关内容:http://hackersdelight.org/ - Paul R
9个回答

43

实际上,在C语言中,这种行为是未定义的。根据C99标准的第7.20.6.1/2节:

abslabsllabs函数计算整数j的绝对值。如果结果不能被表示,则行为未定义。

其注释如下:

在二进制补码表示法下,最负的数的绝对值不能被表示。


4
非常赞同指出整个事情的未定义性,而不是花费很多篇幅解释某个平台刚好从中获得了什么利益。 - DevSolar

14

对于32位数据类型,没有表达+2^31的方式,因为最大的数字是2^31-1...请阅读有关二进制补码的更多信息...


谢谢,我明白了。但是,你的意思是说“没有2^31的表达式”吗? - sigjuice
4
32位数据类型的取值范围是-2^31到2^31-1......因此,是的,没有表达式可以表示2^31,否则会导致溢出。 - tanascius

10

由于整数在内存中以二进制补码形式存储,因此最小值的正版本会溢出并返回负数。

也就是说(在.NET中,但仍然适用):

int.MaxValue + 1 == int.MinValue  // Due to overflow.

并且

Math.Abs((long)int.MinValue) == (long)int.MaxValue + 1

8
显然,从数学上讲,|−231|的值为231。如果我们有32位来表示整数,我们最多可以表示232个数字。如果我们希望表示是关于0对称的,我们需要做出一些决定。
对于以下内容,与您的问题一样,我假设数字的宽度为32位。至少需要使用一个比特模式来表示0。因此,剩下的数字只能用232−1个或更少的比特模式来表示。这个数字是奇数,因此我们可以选择一个不完全关于零对称的表示方式,或者使一个数字具有两种不同的表示方法。
  • 如果我们使用补码表示法,最高位表示数字的符号,其余位表示数字的大小。在这个方案中,0x80000000表示“负零”(即零),0x00000000表示“正零”或普通零。在此方案中,最大正数是0x7fffffff(2147483647),最小负数是0xffffffff(−2147483647)。这种方案的优点是易于“解码”,并且是对称的。这种方案的缺点是当ab为不同符号时计算a + b是一个特殊情况,必须特别处理。
  • 如果我们使用反码表示法,最高位仍然表示符号。正数的该位为0,其余位组成数字的大小。对于负数,只需从相应正数的表示中反转位(取一个长串1的补数-因此称为反码)。在这种方案中,最大正数仍然是0x7fffffff(2147483647),最小负数是0x80000000(−2147483647)。0有两种表示:正零为0x00000000,负零为0xffffffff。这种方案也存在涉及负数的计算问题。
  • 如果我们使用二进制补码方案,负数是通过对反码表示取补码并加1得到的。在这种方案中,只有一个0,即0x00000000。最大正数是0x7fffffff(2147483647),最小负数是0x80000000(−2147483648)。这种表示中存在不对称性。这种方案的优点是无需处理负数的特殊情况。只要结果没有溢出,该表示法就会给出正确的答案。因此,大多数当前的硬件都使用这种表示法来表示整数。

在二进制补码表示中,没有办法表示231。实际上,如果您查看编译器的limits.h或等效文件,您可能会看到对INT_MIN的定义如下:

#define INT_MIN (-2147483647 - 1)

这样做相比于其他方式更加优秀。
#define INT_MIN -2147483648

因为2147483648在32位二进制补码表示中太大了,无法放入一个int类型中。当一元减号运算符“获取”要操作的数字时,为时已晚:溢出已经发生,你无法修复它。
因此,回答你最初的问题,二进制补码表示中最小的负数的绝对值不能用该编码表示。另外,从上面可以看出,在二进制补码表示中从负值到正值的转换,需要先取反再加1。所以,对于0x80000000:
1000 0000 0000 0000 0000 0000 0000 0000   original number
0111 1111 1111 1111 1111 1111 1111 1111   ones' complement
1000 0000 0000 0000 0000 0000 0000 0000   + 1

你会得到原始数字。


你发表的评论非常好,@gbarry++(这个评论是否定了某些东西;我不确定)。 - James McNellis

3

这与数字的存储方式有关。

负数使用二进制补码表示。算法如下...

先将所有位取反,再加1。

以八位数为例...

+0 = -0

00000000 -> 11111111, 11111111 + 1 = 100000000

(但由于比特限制,结果变为00000000)。

还有...

-128 [也称为-(2^7)] 等同于 -(-128)

10000000 -> 01111111, 01111111 + 1 = 10000000

希望这可以帮到你。


3
两个补码数的表示中最高位是负数。0x80000000 是 1 接着 31 个零,第一个 1 表示 -2^31 而不是 2^31。因此没有办法表示 2^31,因为最高正数是 0x7FFFFFFF,这是 0 接着 31 个 1,等于 2^31-1。
在两个补码中 abs(0x80000000) 是未定义的,因为它太大了,因此计算机只会给出 0x80000000。通常至少是这样。

1

我认为abs的工作方式是首先检查数字的符号位。如果它是清晰的,那么不做任何操作,因为数字已经是+ve,否则返回数字的2's complement。在你的情况下,数字是-ve,我们需要找到它的2's complement。但是0x80000000的2's complement恰好是0x80000000本身。


1
那种检查非常不可能发生。这样的检查是完全无用的 - 结果是相同的 - 同时为每个调用消耗额外的时间。这并不是成本和收益之间很好的权衡。 - Konrad Rudolph
1
嗯,你的意思是检查数字是否已经是正数了吗?但是如果你对一个正数取2的补码,你会得到负数,而不是绝对值。 - Jay

1

0x8000.. 存储为 10000....(二进制)。这被称为二补数,意思是最高位(左边的那个)用于存储值的符号,负值使用负二进制 -1 进行存储。abs() 函数现在检查signbit,看到它被设置并计算出正值。

  • 为了获取正值,首先对变量中的所有位取反,结果为01111...
  • 然后加上1,再次得到1000... 即我们开始的0x8000...

现在这又是一个负数,这不是我们想要的,原因是溢出,试试数字 0x9000...即 10010...

  • 对位取反的结果是 01101...
  • 加1的结果是01110...
  • 这是一个正数 0xE000...

这个数字的溢出已经被右侧的0位停止了。


0

因为它使用负指令来执行此操作。

在《汇编语言艺术》一书中,他们就是这样说的。

如果操作数为零,则其符号不会改变,但这会清除进位标志。对任何其他值进行取反都会设置进位标志。对包含-128的字节、包含-32,768的字或包含-2,147,483,648的双字进行取反不会改变操作数,但会设置溢出标志。Neg 操作总是更新 A、S、P 和 Z 标志,就像您使用 sub 指令一样

来源: http://www.arl.wustl.edu/~lockwood/class/cs306/books/artofasm/Chapter_6/CH06-2.html#HEADING2-313 因此,它将设置溢出标志并保持沉默。这就是原因。


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