题记:10/4/2021 刷 LeetCode 时所遇,被一个奇怪的 case 阻挠,无法 AC
涉及:【原码/补码】【C 库函数 abs】
以下代码执行后结果为?
#include <cstdlib>
#include <iostream>
using namespace std;
int main()
{
int val = (1 << (sizeof(int)*8 - 1) ); // -2147483648 = -2^31
val = abs(val);
cout << val << endl;
}
正确答案:-2147483648 ( abs() 处理后,val 没有发生任何变化)
(⊙ˍ⊙)?WHY?WHY?
迷雾追踪
库函数 std::abs()
是绝对值函数,返回传入参数的绝对值!
但很显然,在上述代码中,std::abs()
的取绝对值的能力失效了,所以当务之急是找出 std::abs()
的实现。
(因为 std::abs()
是库函数,而库函数是有 C 标准协会制定函数实现的标准,具体的实现内容,各个平台实现的可能不一致,这里我追寻的是 Linux 平台下的实现版本)
#include <stdlib.h>
#undef abs
/* Return the absolute value of I. */
int
abs (int i)
{
return i < 0 ? -i : i;
}
return i < 0 ? -i : i
??? (⊙x⊙;) ???
-(-2147483648) 不等于 2147483648 吗?难道说,C 语言中的对数取负值的操作并没有我想的那样?
先把自己知道的罗列下:
- int 类型的变量大小 4 byte
- int 这类的整形变量,采用 “补码” 表示负数
- int 能够表示的数据范围:[ -2^31, -1 ] & [ 0, 2^31-1 ]
int val = 1;
val = -val; // 这个过程是什么样的呢?
在计算机组成的相关书籍中有讲过 “负数用补码表示,补码 = 原码取反 + 1”。
这里我们把上述的理论直接带入看看会得到什么样的结果。
int val = 1; 0000:0001
val = -val; 1111:1111 [正确结果]
/*
1. 取反 0000:0001 -> 1111:1110
2. 加一 1111:1110 -> 1111:1111 (吻合)
*/
这里我们仅测试了 “正数 -> 负数”,下面我们再测试下 “负数 -> 正数”
int val = -1; 1111:1111
val = -val; 0000:0001 [正确结果]
/*
1. 减一 1111:1111 -> 1111:1110
2. 取反 1111:1110 -> 0000:0001 (吻合)
*/
现在把 2147483648
带入我们刚掌握的计算过程中:
-2147483648 == 10-00-00-00
1. 减一 10-00-00-00 -> 7f-ff-ff-ff
2. 取反 7f-ff-ff-ff -> 10-00-00-00
_(:з)∠)_ AWSL。。。
按照 C 中取负的处理方法,-2147483648 确实会等于自身。
Emmmm,但是为什么会在出现这样的情况呢?貌似这个问题又回到计算机组成原理了。
当时学编码这块时,就学的不是很通透,看来这回要补课啦 >︿<
(以下为分析过程)
int type size = 4 byte = 4 * 8 = 32 bit = 2^32 种可表达数据
数据范围稍微有亿点大,我缩小点:int type size = 0.5 byte = 4 bit = 16 种可表达数据
1111 [-1]----
1110 [-2] |
1101 [-3] |
1100 [-4] |
1011 [-5] |
1010 [-6] |
1001 [-7] |
1000 [-8] |
========= |
0111 [7] |
0110 [6] |
0101 [5] |
0100 [4] |
0011 [3] |
0010 [2] |
0001 [1] |
0000 [0]-----
负数 -> 正数:-1、取反
正数 -> 负数:取反、+1
其实,这里最应该疑惑的是:
- 为什么我要 “取反”
- 为什么我要 ”+1“
现在请带着这些疑问看向上面的代码框,仔细分析下里面的规律。
不难发现:“补码表示的负数,跟正数,刚好差一格,就可以轴对称”!!!
这个时候再回过来看 C 语言提供的正负转换机制,可以发现 “+1/-1” 刚好就是为了让正负补齐差距,来达成轴对称的目标。
eg:
1 to -1
1> 1 取反,得 -2 【0001 -> 1110】
2> -2 加一,的 -1 【1110 -> 1111】
总结:
迷雾追踪总共提炼了两个问题作为道标:
- 为什么 val = -2147483648 时,std::abs() 库函数会失效
- 为什么 C 语言提供的 “取负” 操作,会让 -2147483648 等于 ta self
问题 ① 解题后的报酬:
- “获得了 Linux 提供的 std::abs() 的具体实现”
- “C 的取负值操作是如何执行的”
问题 ② 解题后的报酬:
- “彻底明白了计算机组成中 补码 & 原码 换算公式是如何推导出来的”