为啥 abs(0x80000000) == 0x80000000?

Posted

技术标签:

【中文标题】为啥 abs(0x80000000) == 0x80000000?【英文标题】:Why is abs(0x80000000) == 0x80000000?为什么 abs(0x80000000) == 0x80000000? 【发布时间】:2011-02-02 02:36:56 【问题描述】:

我刚开始阅读Hacker's Delight,它将 abs(-231) 定义为 -231。这是为什么呢?

我在几个不同的系统上尝试了printf("%x", abs(0x80000000)),但在所有系统上都得到了 0x80000000。

【问题讨论】:

+1 阅读 Hacker's Delight @Paul 谢谢!我几乎没有完成第 1 章。 读完这本书后,请访问网站以获取更多同样的好东西:hackersdelight.org 【参考方案1】:

实际上,在 C 中,行为是未定义的。来自 C99 标准,§7.20.6.1/2:

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

及其脚注:

最大负数的绝对值不能用二进制补码表示。

【讨论】:

绝对 +1 指出了整个事情的不确定性,而不是竭尽全力解释某个平台碰巧做了什么。【参考方案2】:

对于 32 位数据类型,没有 +2^31 的表达式,因为最大的数字是 2^31-1 ...阅读更多关于 two's complement ...

【讨论】:

谢谢。我知道了。但是,您的意思是说“没有 2^31 的表达式”吗? @sigjuice:32 位数据类型的范围是 -2^31 到 2^31-1 ...所以,是的,没有 2^31 的表达式 - 它会导致溢出【参考方案3】:

由于整数作为二进制补码存储在内存中,因此最小值的正版本溢出回负。

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

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

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

【讨论】:

【参考方案4】:

显然,在数学上,|−231|是 231。如果我们有 32 位来表示整数,我们最多可以表示 232 个数字。如果我们想要一个关于 0 对称的表示,我们需要做出一些决定。

对于以下内容,如您的问题所示,我假设为 32 位宽的数字。 0 必须至少使用一个位模式。所以剩下的数字只有 232-1 个或更少的位模式。这个数字是奇数,所以我们可以有一个关于零不完全对称的表示,或者用两种不同的表示来表示一个数字。

如果我们使用 sign-magnitude 表示,最高有效位表示数字的符号,其余位表示数字的大小。在此方案中,0x80000000 是“负零”(即零),0x00000000 是“正零”或常规零。在这个方案中,最大的正数是0x7fffffff(2147483647),最大的负数是0xffffffff(-2147483647)。这种方案的优点是我们很容易“解码”,而且是对称的。该方案的缺点是当ab的符号不同时计算a + b是一种特殊情况,需要特殊处理。 如果我们使用 ones' 补码 表示,最高有效位仍表示符号。正数的该位为 0,其余位构成数字的大小。对于负数,您只需将相应正数表示中的位反转(取一长串一的补码——因此得名 ones'补码)。在这个方案中,最大正数仍然是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++(这个评论否定了一些东西;我只是不确定是什么)。【参考方案5】:

这可以追溯到数字的存储方式。

使用二进制补码存储负数。算法是这样的......

翻转所有位,然后加 1。

以八位数字为例...

+0 = -0

00000000 -> 11111111, 11111111 + 1 = 100000000

(但由于位的限制,变成了00000000)。

和...

-128 [又名 -(2^7)] 等于 -(-128)

10000000 -> 01111111, 01111111 + 1 = 10000000

希望这会有所帮助。

【讨论】:

【参考方案6】:

二进制补码数的最高有效位为负数。 0x80000000 是 1 后跟 31 个零,第一个 1 代表 -2^31 而不是 2^31。所以没有办法表示2^31,因为最大的正数是0x7FFFFFFF,0后跟31个1,等于2^31-1。

abs(0x80000000) 因此在二进制补码中未定义,因为它太大了,因此机器只是放弃并再次给你 0x80000000。通常至少。

【讨论】:

【参考方案7】:

我认为abs 的工作方式是首先检查号码的sign bit。如果它明确什么都不做,因为号码已经是+ve,否则返回号码的2's complement。在您的情况下,号码是-ve,我们需要找到它的2's complement。但是0x80000000 的 2 的补码恰好是 0x80000000 本身。

【讨论】:

这种检查不太可能发生。这样的检查是完全没有用的——结果是相同的——同时为每次调用消耗额外的时间。成本和收益之间的权衡不是很好。 嗯,你的意思是检查数字是否已经是正数?但是如果你取一个正数的 2 的补码,你会得到负数,而不是绝对值。【参考方案8】:

0x8000.. 存储为 10000..(二进制)。这称为二进制补码,这意味着最高位(左侧的一位)用于存储值的符号,负值存储为负二进制 - 1。 abs() 函数现在检查符号位,看到它已设置并计算正值。

首先获取正值 否定变量中的所有位, 导致 01111... 然后加 1, 这再次导致 1000 ...... 0x8000...我们从开始

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

否定位会导致 01101... 在 01110 中添加一个结果... 这是0xE000...一个正数

使用这个数字,溢出被右边的 0 位停止

【讨论】:

【参考方案9】:

因为它使用 neg 指令来执行这个操作。

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

如果操作数为零,则其符号为 不改变,尽管这清除了 携带旗帜。否定任何其他值 设置进位标志。否定一个字节 包含-128,一个单词包含 -32,768 或包含 -2,147,483,648 的双字不会更改操作数,但会设置溢出 旗帜。 Neg 总是更新 A, S, P, 和 Z 标志,就好像你正在使用 子指令

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

【讨论】:

以上是关于为啥 abs(0x80000000) == 0x80000000?的主要内容,如果未能解决你的问题,请参考以下文章

为啥 filter + abs 函数没有给出正确的值

为啥 std::abs() 不能与浮点数一起使用

为啥 abs() 和 fabs() 在 C 的两个不同头文件中定义

mov 0x8(%r14,%r15,8),%rax是啥意思

强制在Windows上以32位进程加载DLL以上2GB(0x80000000)

hge source explor 0x8 timer