参考连接:https://www.cnblogs.com/zh-dream/p/11663569.html
一、正数
正数的源码、反码、补码都是相同的。
二、负数
1、负数的源码与反码的相互转换
负数的源码转换为反码:符号位不变,数值位按位取反。
例如:
源码:1000 1100
反码:1111 0011
负数的反码转换为源码:符号位不变,数值位按位取反。
例如:
反码:1011 0101
源码:1100 1010
2、负数的源码和补码的相互转换
负数的源码转换为补码:1、先转换为反码(符号位不变,数值位按位取反)2、在反码的基础上末位加一。
例如:
源码:1010 0101
反码:1101 1010
补码:1101 1011
负数的补码转换为源码:符号位不变,数值位按位取反,末位加一。
例如:
补码:1110 1011
补码取反:1001 0100
源码:1001 0101
3、负数的反码和补码的相互转换
负数的反码转换为补码:末位加一
例如:
反码:1100 1110
补码:1100 1111
负数的补码转换为反码(源码的反码):末位减一
例如:
补码:1100 1110
反码:1100 1101 (借位减)
4、正数取反
先将正数源码按位取反,得到一个负数,由于负数以补码形式存在,再转换为负数的反码,最后末位加一得到补码。
例如:
0000 1100 #源码
1111 0011 #正数取反得到负数的源码
1000 1100 #负数的源码取反后得到反码
1000 1101 #反码加一得到补码
计算机反码存在的必要:
数值在计算机中bai表示形式为机器数du,计算机只能识别0和1,使用的是二进制zhi,而在日常生活中人们使用的是十dao进制,"正如亚里士多德早就指出的那样,今天十进制的广泛采用,只不过我们绝大多数人生来具有10个手指头这个解剖学事实的结果.尽管在历史上手指计数(5,10进制)的实践要比二或三进制计数出现的晚."(摘自<<数学发展史>>有空大家可以看看哦~,很有意思的).为了能方便的与二进制转换,就使用了十六进制(2 4)和八进制(23).下面进入正题.
数值有正负之分,计算机就用一个数的最高位存放符号(0为正,1为负).这就是机器数的原码了.假设机器能处理的位数为8.即字长为1byte,原码能表示数值的范围为
(-127~-0 +0~127)共256个.
有了数值的表示方法就可以对数进行算术运算.但是很快就发现用带符号位的原码进行乘除运算时结果正确,而在加减运算的时候就出现了问题,如下: 假设字长为8bits
( 1 ) 10- ( 1 )10 = ( 1 )10 + ( -1 )10 = ( 0 )10
(00000001)原 + (10000001)原 = (10000010)原 = ( -2 ) 显然不正确.
因为在两个整数的加法运算中是没有问题的,于是就发现问题出现在带符号位的负数身上,对除符号位外的其余各位逐位取反就产生了反码.反码的取值空间和原码相同且一一对应. 下面是反码的减法运算:
( 1 )10 - ( 1 ) 10= ( 1 ) 10+ ( -1 ) 10= ( 0 )10
(00000001) 反+ (11111110)反 = (11111111)反 = ( -0 ) 有问题.
( 1 )10 - ( 2)10 = ( 1 )10 + ( -2 )10 = ( -1 )10
(00000001) 反+ (11111101)反 = (11111110)反 = ( -1 ) 正确
问题出现在(+0)和(-0)上,在人们的计算概念中零是没有正负之分的.(印度人首先将零作为标记并放入运算之中,包含有零号的印度数学和十进制计数对人类文明的贡献极大).
于是就引入了补码概念. 负数的补码就是对反码加一,而正数不变,正数的原码反码补码是一样的.在补码中用(-128)代替了(-0),所以补码的表示范围为:
(-128~0~127)共256个.
注意:(-128)没有相对应的原码和反码, (-128) = (10000000) 补码的加减运算如下:
( 1 ) 10- ( 1 ) 10= ( 1 )10 + ( -1 )10 = ( 0 )10
(00000001)补 + (11111111)补 = (00000000)补 = ( 0 ) 正确
( 1 ) 10- ( 2) 10= ( 1 )10 + ( -2 )10 = ( -1 )10
(00000001) 补+ (11111110) 补= (11111111)补 = ( -1 ) 正确
所以补码的设计目的是:
⑴使符号位能与有效值部分一起参加运算,从而简化运算规则.
⑵使减法运算转换为加法运算,进一步简化计算机中运算器的线路设计
参考连接:https://zhuanlan.zhihu.com/p/75581822
一直迷糊的浮点数
之前写了 进制转换,也写了计算机怎么 存整数,那么问题来了,计算机中小数怎么存呢?比如2019.725
怎么放到计算机里呢?-2019.725
又该怎么办呢?
我们有什么
首先想一下我们有什么,计算机怎么存数。假如我们的计算机是存 32 位的,那么它就长下边的样子。
由于我们写数字,习惯于先写高位,所以把最低位画到了最右边。
我们需要做的就是把0
和1
放到上边的方框中,来表示2019.725
。换言之,我们就是需要制定一个规则,把2019.725
转成01
字符串存进去。然后读取的时候,再把01
字符串转成2019.725
显示出来。
思路一
简单粗暴些,我们人为的把32
个位置分成三部分,最高位0
表示正数,1
表示负数,然后是整数部分,再然后是小数部分。
低 10
位存小数部分,接下来的 21
位存整数部分,最高 1
位存符号位。所以我们只需要把2019
和725
分别转成二进制,放进去就可以了。二进制位数不足的,高位补零就可以。
仔细想想,规则还不够完善,如果存10.3
和10.03
,小数部分怎么存呢?肯定不能都存3
,这样两个数就不能区分了。这里我想到两种方案:
倒序
小数部分我们逆转以后去存,10.3
小数部分存3
,10.03
小数部分存30
。
补零
我们的小数部分是10
个比特位,所以最多存 个数,十进制的话就是0 - 1023
。为了方便,把1000
以上的数省略掉,范围变成000-999
,也就意味着我们能精确的表示小数点后三位的数,如果一个数小数位数不足3
位,我们就用零补齐。
10.3
的话,可以看成10.300
,小数部分存300
,10.03
的话可以看成10.030
,小数部分就存30
。
能表示的数字范围
整数部分我们有21
个比特位,能表示的数是 个,十进制的话就是 0 - 2097152
。
小数部分如果用的倒序方案,那么我们的范围就是0 - 1023
。
综上,数字范围就是 -2097152.1023 ~ 2097152.1023
。
应用
MySQL
中的定点数DECIMAL
就是采取的上边的思想,将整数部分和小数部分分别存储。不同之处是MySQL
采用了变字长存储,根据十进制的大小,利用不同的字节去存储,所以理论上它能存的范围是无限的。
详细的介绍可以参考 你可能不知道的MySQL中的定点数类型。
思路二
上边的方案有一个问题,表示的数字的范围有些小,最大也才两百多万。限制范围的原因就是,上边的方案整数部分只用了21
个比特位去存。
换一种思路,我们通过移动小数点把小数转成整数,然后再记录小数点移动的位数。
我们用3
个比特位来记录移动的位数,这样我们就可以把小数点移动7
位。此外,整数部分就可以有28
个比特位来表示了,范围大大增加。
其实可以看做是科学计数法的形式,比如2019.725
,可以看做是 ,我们只需要把 2019725
和指数3
存起来就可以了。
能表示的数字范围
28
个比特位来表示整数部分,能表示的数的个数就是 ,对应的十进制范围就是0 - 268435455
。
又因为最多可以移动 7 位的小数,所以数字范围就是下边的
-268435455.0000000 ~ 268435455.0000000
。
可以看到比之前的方案扩大了两个数量级,精度也提高了。但是我们要意识到一件事情,两种方案都用了32
个比特位,所以最多就是表示 个数。思路二表示的范围虽然扩大了,并不是这个范围内的所有数都能精确表示,能表示的数的范围是0 - 268435455
,也就是能表示的有效数字最多只有九位。
对于思路一的最大数2097152.1023
,如果用思路二,虽然它在-268435455.0000000 ~ 268435455.0000000
之内,但是由于它的有效数字位数是11
位,但我们最多存储9
位的,所以我们只好把小数点后的最后两位舍去,存储2097152.10
。
应用
对于C#
中的System.Decimal
是用的类似于思路二的思想,具体的话参考 没有神话,聊聊decimal的“障眼法”。
思路三
上边的思路一和思路二都是站在十进制数的角度上先考虑对数字的分割、转换,然后将十进制的整数转为二进制进行存储的。再换一种思路,先把十进数转成二进制数,再去存呗。
对于2019.725
转成二进制就是11111100011.1011100110...
,为啥出现了省略号?进制转换中已经讨论过这个问题了,那就是大部分十进制数并不能精确的转到二进制,这是一个固有的事实,我们也没啥办法,假装没有省略号,继续讨论吧。
尾数部分
这里存储的话,可以借鉴思路二,我们可以把二进制小数转换成科学计数法的形式,统一规格再去存储。这里用个小技巧,我们知道所有的数字除了0
之外,一定会含有一个1
,所以我们把数字转成下边的形式。
为什么要用上边的形式呢,这样做的一个好处就是存的时候,我们只需要存xxxxxxxxx...
的部分,显示数字的时候再考虑它的最高位还有一个1
。这样如果用23
个比特位来存数字,相等于存储了24
位。
指数
区别于思路二的指数,思路二我们是把原来的小数转为整数,所以指数一定是个负数,直接存它的绝对值就行。但在这里如果之前的数字是 0.000001
这样的,那么指数E
是负数,但如果是1110011.1
这样的,指数E
就是正数了。
如果指数E
是用8
个比特位来存储,那么总共就是 个数,范围就是0 - 255
,那么怎么存负数呢?最简单的方案,人为规定呗,将一部分正数映射到负数上,就对半分吧。
0 1 125 126 127 128 129 254 255
-127 -126 ... -2 -1 0 1 2 ... 127 128
要表示的指数加上127
映射到0 - 255
。反过来显示指数真正值的时候,需要减去127
。
综上所述,我们就用 1
个比特位来存符号位,8
个比特位来存指数,存的时候记得加上偏移,剩下的23
个比特位来存有效数字,而事实上我们其实可以看做存了24
位。
这样的话,求出2019.725
的二进制是 11111100011.1011100110011
。把它规格化,小数点需要向左移动10
位,也就代表指数是 10
,存的话还需要加上127
,也就是137
了,二进制表示是10001001
。所以规格化就是下边的数了:
所以计算机中就是下边这样存的了。
特殊数字
我们还有个问题没有解决,上边规格化的时候我们默认了所有数字至少有一个1
,但是0
怎么办呢?规则是我们自己定的,继续定呗。
当指数是-127
时,一个很小很小的数了,此时对应的指数加上127
,也就是0
。我们规定当E
为0
并且M
全为0
,我们就说这个数字是0
。
-127
用了以后,负指数最小就是-126
了。
让我们再规定几个特殊的数字。
当指数是128
时,一个很大很大的数,也就是当 E
全为1
。分几种情况。
- 有效数字
M
每一位全为0
,
S
为0
,就代表正无穷。
S
为1
,就代表负无穷。
- 有效数字 M 每一位不全为 0,代表
NaN
,表示不是一个数字。
所以此时正指数最大就是127
了。
能表示数的范围
最大数和最小数
最大的数,指数最大取1111 1110
,也就是254
,减去127
,代表指数是127
。有效数字全部取1
,也就是1.111...1
,小数点后边23
个1
。然后把它转成十进制的话,用一个技巧,先给它加 ,这样所有的1
都产生进位,变成了10.0000000
,这样的话转成十进制就是2
了,当然我们还要减去 。
综上,最大的数字用十进制表示就是 ,也就是 。
所以能表示的范围就是 到 。
再看一下能表示的最小正数。指数取最小为 0000 0001
,减去127
,代表指数是-126
。有效数字全部取0
,其实也就代表1.000...0000
,总共有23
个0
,转成十进制的话就 ,也就是 。
有没有发现一个问题,我们要找最小的数,但因为之前的规定,最高位却能是1
。但理论上我们找一个0.000...1
这样的数才代表最小的正数。怎么得到这样的数呢?
再加个规则吧。当时我们规定当E
为0
的并且M
每一位全为0
,我们就说这个数字是0
。那M
不全为零呢?
之前是假设省略了最高位1
,我们加一个新规则,当E
全为0
时,我们就说最高位不再省略1
。所以最小的尾数就可以是0.00...1
了,小数点后22
个零,转成十进制就是 。指数还是之前的-126
,所以比之前更小的正数就是 ,也就是 。
精度
我们用23
个比特位表示尾数部分,最多能表示 个数,也就是8388607
,从十进制的角度,所以它的有效数字最多有7
位。
应用
上边介绍的其实就是IEEE 754
标准了,计算机中存储32
位浮点数基本上都是这个规则。比如java
中的float
类型,可以看一下jdk
源码中的一些定义。
/**
* A constant holding the largest positive finite value of type
* {@code float}, (2-2<sup>-23</sup>)·2<sup>127</sup>.
* It is equal to the hexadecimal floating-point literal
* {@code 0x1.fffffeP+127f} and also equal to
* {@code Float.intBitsToFloat(0x7f7fffff)}.
*/
public static final float MAX_VALUE = 0x1.fffffeP+127f; // 3.4028235e+38f
/**
* A constant holding the smallest positive normal value of type
* {@code float}, 2<sup>-126</sup>. It is equal to the
* hexadecimal floating-point literal {@code 0x1.0p-126f} and also
* equal to {@code Float.intBitsToFloat(0x00800000)}.
*
* @since 1.6
*/
public static final float MIN_NORMAL = 0x1.0p-126f; // 1.17549435E-38f
/**
* A constant holding the smallest positive nonzero value of type
* {@code float}, 2<sup>-149</sup>. It is equal to the
* hexadecimal floating-point literal {@code 0x0.000002P-126f}
* and also equal to {@code Float.intBitsToFloat(0x1)}.
*/
public static final float MIN_VALUE = 0x0.000002P-126f; // 1.4e-45f
可以看到float
的最大值,和正数的两个最小值和我们分析的完全一致。
当然,上边的我们分析的是32
位的情况,64
位的话和32
位基本是一样的,对应于java
中的double
类型。
除了位数变多以外,其它的分析和32
位都是一样的,此时指数不再加127
,而是加1023
。
一些问题
0.1 + 0.2 == 0.3 //false
这样就可以理解上边的经典问题了,之所以两边不相等,第一个原因就是大部分十进制并不能精确的转为二进制,第二个原因就是存储的位数有限,会进行一些舍去。
如果对于一些能精确表示的浮点数,判断相等就不会出问题了。
0.125 + 0.125 == 0.25 //true
另一个问题,当我定义一个变量
float a = 0.1;
既然不能精确表示0.1
,那么我们把它输出的时候为什么显示的是0.1
。
在 进制转换 中我们算过,0.1
转成二进制的话是0.0 0011 0011 0011...
,因为我们只能存23
位有效数字,所以我们只能存0.000 1 1001 1001 1001 1001 1001 101
。
因为我们最高位可以省略个 1
,所以就是第一个1
后边有23
位。
此外,最后 3
位本来是100
,因为100
后边一位是1
,所以相当于产生进位变成101
。
这个二进制数还原为10
进制的话就是0.10000000149011612
。
让我们来验证一下
System.out.println(String.format("%.18f", 0.1f));
//0.100000001490116120
和我们预想的完全一样,而如果不加限制直接输出0.1
,显示0.1
只是因为默认的有效位数比较少,其实只是输出了近似值0.1
。
结束
浮点数的探究到此结束了,回顾一下,前两种方法我们先从十进制的角度来考虑,通过存整数来存小数,从而保证了我们可以精确存数。思路三,我们先把十进制转成二进制去存,这就产生了不能精确的存储数的问题。同时有两个技巧也值得注意,第一个就是用存储指数的时候,我们用正数表示负数。第二个就是,默认规定最高位是1
,从而用23
个比特位存了24
位的数据。
文章被以下专栏收录
推荐阅读
详解二进制浮点数
(二十六)通俗易懂理解——浮点与定点的计算机表示及互转
在神经网络当中,为了尽快落地就需要考虑到数据存储以及速度问题,这时候将浮点数转为定点数就是一种比较常规的做法,也就是涉及到Binary neural networks和quantization,这部分有待下一篇…
浮点数精度问题透析:小数计算不准确+浮点数精度丢失根源
在知乎上上看到如下问题:浮点数精度问题的前世今生?1.该问题出现的原因 ?2.为何其他编程语言,比如java中可能没有js那么明显3.大家在项目中踩过浮点数精度的坑?4.最后采用哪些方案规避…
11 条评论