计算机的存储规则(反码补码浮点编码)
Preface: 如果你觉得本篇文章哪里有错误,可以告诉我,谢谢ღ( ´・ᴗ・` )比心
一、补码反码源码
原码:即一个字表示为二进制后,例如2的源码就是0011
反码:对原码进行取反操作,也就是0变1,1变0
例如:
2的源码是:0011
2的反码是:1100
补码:
1、先让我们做一个减法运算:9-1
我们可以很轻松的得到9-1=8,
接下来我们尝试一些很神奇的东西:
9-1
=9+99
=108
我们可以发现尾数是一样的,所以如果我们限制了能显示的位数(比如两位)那么我们就可以发现我们能把减法做成加法
也就是
9-1 = 9+99 = 108,
因为只保留两位,所以舍弃1,其实就变成了08
即:9-1 = 9+99 = 08
聪明的你可能依旧发现了,其实99是因为100-1得到的,那么现在做一点类似的题目:
80-20
=80+(100-20)
=80+80
=160
去掉第1位(也就是最高位),我们又可以得到60这个准确的答案,这是偶然吗,不,其实这是有原因的。
让我们看一下下面这个圆:
9 0
8 1
7 2
6 3
5 4
它就像时钟一样,我们假设要从9走到2,我们无论是顺时针走,还是逆时针走都可以走到2,如果你对数字很敏感的话,你应该已经发现了,所谓的逆时针其实就是做减法,顺时针其实就是做加法。
也就是:
9顺时针走3步,就会走到2,即可以表示为:9+3=2
9逆时针走7步,也会走到2,即可以表示为:9-7=2
那么走的步数之间有什么关系呢,我们可以看到3+7=10,也就是刚好是我们的最高位9+1
因此我们在做个例子:
9要到3可以表示为:9+4 = 3
或者是9 + (10-4) = 3
而10-4也就是6就叫做4的补码
当然你反过来说4是6的补码,这也是一样的。
现在你已经很轻松的可以获得补码了,但是如果我们想要加入负数呢?我们可以把0-9拆成两半,也就是0-4是整数,59变成-1-4
这样上面的圆就变成了:
-5 0
-4 1
-3 2
-2 3
-1 4
科学家其实也就是这么定义的,
例如8位的数字中
让 0000_0000 ~ 0111_1111表示为正数
让 1000_0000 ~ 1111_1111表示为负数
所以现在我们澄清了一个误会,第一位数字所代表的是符号位(1表示负数,0表示负数)
现在我们相信你已经知道了补码的作用了。是为了使用加法来替代减法的数字。
我们如果通过上网查资料,其实我们可以很轻松的就了解到 补码 = 反码 + 1,那么这是为什么呢?
我们在利用一个和上面类似的圆(这一次我们采用8进制,之所以这样采用是因为10进制与2进制不好对应,如果进行反码运算你很难看出结果,当然你也可以采用16进制,但是这样需要画更大的圆):
7 0
6 1
5 2
4 3
如果你通过计算,你可以发现一个很神奇的规律0的反码是7,1的反码是6……,也就是所每一行所对应的数字就是它自己的反码。
7<--0
6<----------1
5<----------2
4<--3
通过上面的图,我们可以很直观的看到反码的对应关系,
现在我们来看看补码的对应关系。
3的补码是5,2的补码是5,1的补码是0,所以其实我们可以发现一个规律,就是所谓的补码刚好是反码的下一位
我们通过之前的学习其实已经知道了,所谓的下一位可以表示为顺时针走1步,也就是+1,看到这里我相信你已经明白了为什么补码 = 反码+1
补充:
1、通过上面的学习我们已经知道了其实我们对第一位是符号位是有误解的,但是其实我们可以很显然的发现第一位是1开头的是负数,例如在8位的数字中:
1000_0000 ~ 1111_1111所代表的是负数。所以即使我们把第一位当成符号位也无妨。
2、通过之前的学习我们知道其实数字被分成了两部分,正数是从0开始的,而负数是从1开始的,例如:
-5 0
-4 1
-3 2
-2 3
-1 4
那么了解这点对我们有什么用处呢?
它可以帮助我们了解到数据的取值范围。
例如int表示为4个字节,也就是48=32位,但是数字被划分成了两半,所以以正数有31位,负数有31位
所以对于int来说它可以表示的整数有231个,但是对于正数来说我们需要从0开始,因此最大的数其实就是231 - 1
但是对于负数来说,是从1开始的,所以负数的最大数的不用-1的,也就是2^31,现在我相信你已经能很轻松的写出任意一个整数类型的取值范围了吧。
例如:long是8个字节,那么指数就是88-1=63,所以long的取值范围为: -2^63 ~ 2^63 - 1
二、浮点数
存储方式:
以下以float的存储为例,double也是类似的,只不过所占字节较长。
对于任意的一个浮点数,它可表示为:
(-1)^S * M * 2^E
- S为0时,表示一个正数,S为1时表示一个负数
2)M表示的是一个有效数字 - E表示的是指数,就像在10进制中的10^E是一个道理
对于float来说,一共占32个字节:
S占第1个字节
E占8个字节(即:第2~9个字节表示的是E)
M占剩下的所有字节(23个字节)
如下图:
0_00000000_00000000000000000000000
| | |
S_E(8 bit)_M(32 bit)
1、M的取值是怎么取的
M的值规定一定是1<=M<2,所以M一定可以写成1.xxxxxx的数,例如0101可以写成1.01,剩下的在依靠指数去表示,我们先不进行考虑,M就是一个有效数字。
因此规定M在存储的时候舍弃第一个1,这样我们就可以多存储一位,当转换回来的时候,再去添加上。这样原本的23位,就变成可以存储24位的信息。
2、指数E
E是一个指数,就像在科学计数法中,我们使用1.5*10^-1来表示0.15一样,只不过因为计算机中是二进制,所以底数是2。
因为E占8位,且可正可负,通过之前整数的存储知识我们就能了解到E的取值范围是 -2^7 ~ 2^7 -1 也就是: -128 ~ 127
3、小数位如何转换为二进制。
以前我们只知道整数转换为二进制需要不断的除2取余,那么小数该怎么转换为二进制呢,它与整数部分刚好反过来,不断的乘2,取整数部分。
举个例子:
0.375表示为二进制就是
0.375 * 2 = 0.75 取整数部分0
0.75 * 2 = 1.5 取整数部分1
0.5 * 2 = 1.0 取整数部分1
而后的0在怎么乘就都是0了,所以0.375转换为二进制就是0.011
存储示例:
了解了上面的知识后,我们现在做个例子,假设存储一个数字1.375
首先整数部分转换为二进制后为1
小数部分转换为二进制为0.001
所以1.375的有效数字M为1.001即:1001
而在存储的时候M要去掉第一位1,所以存储的有效数据数据应该是001
指数位E = 0
符号位S = 1
所以整个存储的内容就是:
1_0000 0000_0000 0000 0000 0000 0000 001
精度缺失:
学习了上面的小数二进制转换后,我们现在尝试0.2进行二进制转换
0.2 * 2 = 0.4 --> 0
0.4 * 2 = 0.8 --> 0
0.8 * 2 = 1.6 --> 1
0.6 * 2 = 1.2 --> 1
0.2 * 2 = 0.4 --->0
算到这里,我们可以发现其实我们又回到了0.2,小数位永远无法变成0。
因此0.2转换为二进制后的数是:0.00110……,0011循环
但是我们的计算机中能存储的位数是有限的,因此最后只能选择舍弃一部分,就像三分之一所表示的0.333333……,3无限循环一样。而且是直接舍去,并不会进位,我想这一点在说整数的存储的时候,你已经深有体会。
那么也就是说,我们最后只能得到一个比原本数值小一点点的数,但是这也无伤大雅。这就是精度缺失。
补充:
1、如果判断一个浮点数是否等于0
因为精度缺失后会导致一个数字比原本小一点点,所以我们不妨这样进行判断:
if( (num <= 0.0) || (num >= -0.000001) ){
该数字等于0
}
三、编码
编码其实就是规定了一个字符底层存储了什么东西,所以不同的编码会导致存储的方式不一样,例如在ASSCII中a存储的数据转换成10进制出来是97,但是在其他编码中就不一定了,所以当你对比两个字符的时候一定要注意他们的编码是否相同,否则即使看起来一样的字符,他们也是不相等的。
推荐阅读