为啥 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:
abs
、labs
和llabs
函数计算整数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)。这种方案的优点是我们很容易“解码”,而且是对称的。该方案的缺点是当a
和b
的符号不同时计算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?的主要内容,如果未能解决你的问题,请参考以下文章
为啥 abs() 和 fabs() 在 C 的两个不同头文件中定义