C语言标准并未要求用补码形式来表示有符号整数,但是几乎所有机器都是这么做的。补码有两个优点:一个是能够计算减法,另外一个就是统一正零和负零。
——《深入理解计算机系统》
二进制数与十进制数一样有正负之分。在计算机中,常采用数的符号和数值一起编码的方法来表示数据。常用的有原码、反码、补码等。这几种表示法都将数据的符号数码化。为了区分一般书写时表示的数和机器中编码表示的数,我们称前者为真值,后者为机器数或机器码。
原码,反码,补码的产生过程,就是为了解决,计算机做减法和引入符号位(正号和负号)的问题。
由计算机的硬件决定,任何存储于计算机中的数据,其本质都是以二进制码存储。
根据冯~诺依曼提出的经典计算机体系结构框架。一台计算机由运算器,控制器,存储器,输入和输出设备组成。其中运算器,只有加法运算器,没有减法运算器(据说一开始是有的,后来由于减法器硬件开销太大,被废了 )
所以,计算机中的没法直接做减法的,它的减法是通过加法来实现的。你也许会说,现实世界中所有的减法也可以当成加法的,减去一个数,可以看作加上这个数的相反数。当然没错,但是前提是要先有负数的概念。这就为什么不得不引入一个该死的符号位。
用最高位表示符号位,‘1’表示负号,‘0’表示正号。其他位存放该数的二进制的绝对值。
以八位有符号数二进制表示为例
十进制 | 二进制 |
---|---|
+0 | 0000 0000 |
-0 | 1000 0000 |
1 | 0000 0001 |
-1 | 1000 0001 |
2 | 0000 0010 |
-2 | 1000 0010 |
3 | 0000 0011 |
-3 | 1000 0011 |
… | … … |
原码表示法直观易懂,于是,我们开始用其运算
十进制 | 二进制(原码) | 结果(原码真值) | 正确与否 |
---|---|---|---|
1+2 | 0000 0001 + 0000 0010 = 0000 0011 | 3 | √ |
(+0) + (-0) | 0000 0000 + 1000 0000 = 1000 0000 | -0 | 姑且算对吧 |
1+(-1) | 0000 0001 + 1000 0001= 1000 0010 | -2 | ???不行呀 |
由此可见,使用原码进行正数加法是可以的,但是当涉及负数时,结果就有点莫名其妙了——于是,反码来了。
反码:正数的反码还是等于原码
负数的反码就是他的原码除符号位外,按位取反。
因为人脑可以知道第一位是符号位,可以根据符号位对真值的绝对值进行加减乘除,但是对于计算机来说,加减乘除是最最最基本的运算,要设计的尽量简单,计算机辨别符号位会让计算机的设计电路变得很复杂,于是人们想出了让符号位也参与到运算上来。减去一个数,等于加上他的负数。
使用原码参加运算的缺陷
正数(原码) | 负数(原码) |
---|---|
0000 0000 (0) | 1000 0000 (-0) |
0000 0001 (1) | 1000 0001 (-1) |
0000 0010 (2) | 1000 0010 (-2) |
0000 0011 (3) | 1000 0011 (-3) |
… | … |
0111 1101(125) | 1111 1101(-125) |
0111 1110(126) | 1111 1110(-126) |
0111 1111(127) | 1111 1111(-127) |
从上面的原码表中可以看见左边每增加一个二进制单位对应的真值是递增的,而右边每增加一个二进制单位对应的真值是递减的,所以对于原码来说,能满足正数的加法,但无法满足负数的加法。
为了满足负数对加法的需求,就必须让负数与他对应的二进制码是同步递增或者同步递减。
于是就通过符号位不变,其余位取反来满足这个同步递增或者递减的要求,由于正数本来就满足它本身的加法,所以不需要做任何改变。这就是反码的定义由来。
正数(反码) | 负数(反码) |
---|---|
0000 0000 (0) | 1111 1111 (-0) |
0000 0001 (1) | 1111 1110 (-1) |
0000 0010 (2) | 1111 1101 (-2) |
0000 0011 (3) | 1111 1100 (-3) |
… | … |
0111 1101(125) | 1000 0010(-125) |
0111 1110(126) | 1000 0001(-126) |
0111 1111(127) | 1000 0000(-127) |
从上图的反码表中可以看到在运算不跨过0的时候,正负数的加法已经能满足要求
十进制 | 二进制(反码) | 结果(反码真值) | 正确与否 |
---|---|---|---|
1+(-2) | 0000 0001 + 1111 1101 = 1111 1110 | -1 | √ |
2+(-5) | 0000 0010 + 1111 1010 = 1111 1100 | -3 | √ |
127+1 | 0111 1111+ 1000 0001 = 1000 0000 | -127 | 加法算出来是128由于128超过最大值,余1,所以取最小值开始的第一位,也就是最小值-127,但是这里有个不合理的地方,就是[1111_1111]和[0000_0000]都表示0,这导致在实际计算中每当跨过0一次,就有一个单位的误差 |
-1+2=[1111_1110]反+[0000_0010]反=[0000_0000]反=0
要解决这个问题就必须让反码中的[1111_1111]和[0000_0000]合并,补码来了。
由于[1111_1111]+[0000_0001]=[0000_0000],所以在负数反码的基础上+1就可以解决反码中跨0的误差问题,同时不会对负数与它对应的二进制反码的同步递增产生影响,所以在反码的基础上+1就完美的解决了符号参与预算的问题,这就是补码为什么是在负数反码的基础上+1的由来。
假 设 一 个 整 数 数 据 有 w 个 二 进 制 位 , 我 们 可 以 将 整 个 位 向 量 写 成 x ⃗ , 表 示 整 个 向 量 ; 或 者 [ x w − 1 , x w − 2 , . . . , x 0 ] , 表 示 向 量 中 的 每 一 位 。 假设一个整数数据有w个二进制位,我们可以将整个位向量写成\vec{x},表示整个向量;或者 [x_{w-1}, x_{w-2}, ... , x_0],表示向量中的每一位。 假设一个整数数据有w个二进制位,我们可以将整个位向量写成x,表示整个向量;或者[xw−1,xw−2,...,x0],表示向量中的每一位。
对 于 向 量 x ⃗ = [ x w − 1 , x w − 2 , . . . , x 0 ] : 其 补 码 的 值 = − x w − 1 2 w − 1 + ∑ i = 0 w − 2 x i 2 i 对于向量 \vec{x} = [x_{w-1}, x_{w-2}, ... , x_0]: 其补码的值 = -x_{w-1}2^{w-1} + \sum_{i=0}^{w-2}x_i2^i 对于向量x=[xw−1,xw−2,...,x0]:其补码的值=−xw−12w−1+i=0∑w−2xi2i
正数的补码等于他的原码
负数的补码等于反码+1。
负数的补码等于他的原码自低位向高位,尾数的第一个‘1’及其右边的‘0’保持不变,左边的各位按位取反,符号位不变。
https://blog.csdn.net/yohohohoho/article/details/7248143
https://blog.csdn.net/afsvsv/article/details/94553228
https://blog.csdn.net/chenchao2017/article/details/79733278