探索浮点数在内存中的存储(附带快速计算补码转十进制)
目录
一、浮点数在内存中的存储
1、常见的浮点数:
2、浮点数存储规则:
3、内存中无法精确存储:
4、移码与指数位E:
5、指数E的三种情况:
二、快速计算补码转十进制
1、第一种方法讨论:
2、第二种方法讨论:
3、第三种方法讨论:
4、第四种方法讨论:
一、浮点数在内存中的存储
1、常见的浮点数:
首先C语言中的浮点数是什么呢?说白了就是小数,这样的小数在C语言中主要有两种表示:
3.14159//这种是常见的浮点数,以小数形式出现
3.14E10//这种是科学计数法中的表示,浮点数为3.14×10^10,或写成3.14E+10
既然整型在计算机中有表示范围,那么浮点数其实也是有范围的,我们可以在<limits.h>文件和<float.h>文件中查找到整型和浮点型的表示范围。那么整型和浮点型在内存中的存储相同吗?我们根据一段代码进行分析。
int main()
{
int n = 9;
float* pf = (float*)&n;
printf("n = %d\n", n);//打印结果为:n = 9
printf("*pf = %f\n", *pf);//打印结果为:*pf = 0.000000
*pf = 9.0;
printf("n = %d\n", n);//打印结果为:n = 1091567616
printf("*pf = %f\n", *pf);//打印结果为:*pf = 9.000000
return 0;
}
注:上面的代码是在VS编译器的x64环境下的运行结果。
这个代码得到了让人意想不到的结果,第1条和第4条printf()语句的输出结果自然不必多说,那么第2条和第3条的执行结果说明,整型数据和浮点型数据的存储方式是不同的。
2、浮点数存储规则:
根据国际标准IEEE754,可以将任意一个浮点数V都写成(-1)^S×M×2^E,其中(-1)^S表示符号位,当S为0时,V为正数,S为1时,V为负数。M表示有效数字,规定范围在1和2之间,2^E表示指数位。
补充:探究二进制整数和小数的权重
1111 1111 . 1111,整数部分的权重由小到大(从右向左)依次是2^0=1、2^1=2、2^2=4、2^3=8、2^4=16、2^5=32、2^6=64、2^7=128,小数部分权重由大到小(从左向右)依次是2^(-1)=0.5、2^(-2)=0.25、2^(-3)=0.125、2^(-4)=0.0625。
比如5.5,转换成二进制是101.1,这样表示不是科学计数法所规定的,我们将小数点向左移动两位(称为左规两位),变成1.011×2^2(左规次方为正数,右规次方为负数),这样看,V=(-1)^0×1.011×2^2,其中S=0,M=1.011,E=2。
这里我们就可以看出,为什么任何一个浮点数的M都是在1到2之间?因为对于任意一个数转换成二进制,最高位一定是1(因为如果是0可以不写),这样左规几次之后,M得到的值一定在1和2之间。
下面将5.5存储到内存中,IEEE754规则如下,对于32位浮点数(float类型),使用1个比特位用来存放符号位S,用8个比特位存放指数位E,剩下的23个比特位存放有效数字M,但由于M的范围确定,所以最高位的1(整数部分的1)通常不需要存储,所以实际上可以存储24位有效数字。对于64位浮点数(double类型),使用1个比特位存放符号位S,11个比特位存放指数位E,剩下的52个比特位存放有效数字M,同样省略了M的最高位。
3、内存中无法精确存储:
对于一些浮点数,计算机是无法准确进行存储的,比如5.3,它的小数部分0.3转换成二进制可能有非常多的位数(0.3的二进制为 0.010011 0011 0011...,1001部分无限循环),所以计算机存储的也是近似的数据。
//浮点数在内存中的存储
int main()
{
float f = 5.3f;
return 0;
}
4、移码与指数位E:
首先,E表示指数位,就一定是一个无符号整数,如果E有8位,那么范围是0~255,E有11位,范围是0~2047。但是E有可能是负数,比如0.5的科学计数法表示为1.0×2^(-1),此时E=-1。
为了避免这种情况的发生,我们将8位的E加上偏移量127,11位的E加上1023,这样就可以保证E一定为正数,这样的E的二进制表示称为移码表示,所以我们可知,加上偏移量的E的移码就一定为正数,但移码不止在浮点数中表示阶码,其他地方也有所体现,所以移码不一定为正数。
//浮点数在内存中的存储
int main()
{
float f = 5.5f;
//5.5的二进制表示101.1,科学计数法表示1.011×2^2
//S=0(正数),M=011 0000 0000 0000 0000 0000,E=2+127
//二进制0 1000 0001 011 0000 0000 0000 0000 0000
//对齐后结果0100 0000 1011 0000 0000 0000 0000 0000
//对应的十六进制40 b0 00 00
return 0;
}
下面分析我们来最初的代码,代码如下。
int main()
{
int n = 9;
float* pf = (float*)&n;
printf("n = %d\n", n);//打印结果为:n = 9
printf("*pf = %f\n", *pf);//打印结果为:*pf = 0.000000
*pf = 9.0;
printf("n = %d\n", n);//打印结果为:n = 1091567616
printf("*pf = %f\n", *pf);//打印结果为:*pf = 9.000000
return 0;
}
先看第3条printf()语句。
//第3条printf()语句相当于下面的代码
int main()
{
float f = 9.0f;
//9.0的二进制是1001.0,科学计数法表示为1.0010×2^3
//S=0,M=001 0000 0000 0000 0000 0000,E=3+127
//二进制为0 1000 0010 001 0000 0000 0000 0000 0000
//整理后0100 0001 0001 0000 0000 0000 0000 0000
//十六进制为41 10 00 00
//为什么输出1091567616呢?
//这是因为计算机把0100 0001 0001 0000 0000 0000 0000 0000当成有符号数
//2^20+2^24+2^30=1048576+16777216+1073741824=1091567616
return 0;
}
5、指数E的三种情况:
如果拿到一串浮点数的二进制数,计算E就分为下面三种情况。
1)E不为全0,也不是全1:
拿到E的部分,比如1000 0001,用这个数的十进制减去127后,得到的就是E原来的值,即129-127=2,原来的E=2。
2)E为全0:
此时的E如果是8位,则真实值是1-127=-126,如果是16位,则真实值是1-1023=-1022,实际上这个小数已经是非常非常小的一个数了,无限接近于0,那么计算机会将这样的小数按0来处理,这时的有效数字M不再加上第一位的1,而是还原为0.xxxxx...的小数,这样做为了表示±0。也就是E、M全0时,这样的小数就按0处理。
3)E为全1:
如果E全为1,此时M全为0,这样的数字表示±∞,正负取决于符号位S。
这样特殊的数就有8种表示:
S E M 值 0 0 0 +0 1 0 0 -0 0 0 ≠0 非规格化正数 1 0 ≠0 非规格化负数 0 1 0 +∞ 1 1 0 -∞ 0 1 ≠0 NaN 1 1 ≠0 NaN 非规格化数字是计算机中一种特殊的数字。
NaN(非数)也是一种特殊的数字,含义是无定义的数或不可表示的数,比如0/0、∞/∞、∞-∞等等返回的都是一个非数。
下面就可以分析上面代码的第2条printf()语句了。
//第2条printf()语句
int main()
{
int n = 9;
printf("%f\n", n);//打印结果为:0.000000
//9的二进制为0000 0000 0000 0000 0000 0000 0000 1001
//其中E的部分是0000 0000,全为0
//M的部分是000 0000 0000 0000 0000 1001
//科学计数法表示为0.000 0000 0000 0000 0000 1001 × 2^(-126),这已经是非常小的数字了
//而且%f只能打印小数点后六位,结果自然是0.000000
printf("%f\n", (float)n);//打印结果为:9.000000
return 0;
}
二、快速计算补码转十进制
首先,只有负数才有补码,正数的补码就是原码,直接按照原码转换成十进制即可,所以下面计算的均是负数的补码转成十进制(《最快10秒钟就可以完成》)。
我们先看如何转换,之后在讨论是为什么这么转换。
目前,拿到一个负数的补码,我知道的有4种方法可以转换成十进制:
1、补码转成原码,再转换成十进制,比如1100 0101[补] 转成 1011 1011[原],再转换成十进制就是-(1+2+8+16+32)=-59。
2、用最高位的位权依次加上其他位的值,比如1100 0101转换成十进制,-128+(1+4+64)=-59。
3、先求无符号数,再用256减去无符号数的值,最后取负数即可,比如1100 0101转换成十进制,-(256-(1+4+64+128))=-59。
4、直接按照补码进行计算,按照有0的位权相加,再加1,最后取负(这是我认为最快的方法),比如1100 0101转换成十进制,-(2+8+16+32+1)=-59。
个人觉得第1种最常用,但是比较慢,第2、3种实际上差不多(第三种甚至比第二种还慢),第4种最快(因为对我们来说,取负操作要比含有负数的减法更受欢迎)。
下面补充一种更快的计算数字相加的方法,是不是经常对于1+2+8+16+32这样的一串数字相加比较头疼,今天快速计算方法,8以下的数字一起计算(一眼就能算出结果),128以下的数字一起计算(绝大多数都是最大计算到128),那么剩下的就是8+16+32+64这种,只需将16看成2×8,32看成4×8,64看成6×8即可,比如1+2+8+16+32=3+(1+2+4)×8=56+3=59。
下面来讨论到底是为什么可以这么转换,如果对这里不感兴趣的同学,现在可以划走了~
1、第一种方法讨论:
实际上,我也不知道为什么要这样转换,在学计算机的时候,就接触到这种方法了,但是用着用着感觉比较慢,于是开发出了其他的方法。
2、第二种方法讨论:
对于正数的原码,其实每一位是有对应的位权的,比如1001[原],对应的十进制就是2^0+2^3=9。我们知道,对于负数的补码来说,二进制的最高位表示符号位,它的位权是一个负值,比如1001[补],对于的十进制就是-2^3+2^1=-7(每一位的位权不变,只是最高位是个负数)。
3、第三种方法讨论:
这种方法是我观察char类型和unsigned char类型数据的规律中得出的,数据如下:
char二级制(补) | 十进制 | unsigned char二级制(补) | 十进制 |
---|---|---|---|
0000 0000 | 0 | 0000 0000 | 0 |
0000 0001 | 1 | 0000 0001 | 1 |
0000 0010 | 2 | 0000 0010 | 2 |
0000 0011 | 3 | 0000 0011 | 3 |
... | ... | ... | ... |
0111 1110 | 126 | 0111 1110 | 126 |
0111 1111 | 127 | 0111 1111 | 127 |
1000 0000 | -128 | 1000 0000 | 128 |
1000 0001 | -127 | 1000 0001 | 129 |
... | ... | ... | ... |
1111 1100 | -3 | 1111 1100 | 253 |
1111 1110 | -2 | 1111 1110 | 254 |
1111 1111 | -1 | 1111 1111 | 255 |
不难看出,char类型可以存储127个正数,128个负数,还有一个0,一共256个数据,所能表示的范围是-128~127。unsigned char类型可以存储255个正数和一个0,也是256个数据,所能表示的范围是0~255。
同时上面的两组数据也有一些规律,观察有符号和无符号的十进制,当最高位为1时,对应的绝对值之和总是256,比如1000 0001[补]是-127[有]或129[无],可以写成-127 = -(256-129),而129用2^7+2^1计算。
那么以后计算有符号数比较麻烦时,可以这样快速计算,即用256减去对应的无符号数,再取负,得到对应的有符号数。比如1011 0110[补],无符号数是2^1+2^2+2^4+2^5+2^7=2+4+16+32+128=182,有符号数就是-(256-182)=-74。
这种计算方法的优点是把对应的无符号数也计算出来了,缺点还是比较慢。
4、第四种方法讨论:
还有一种计算有符号数的方法,观察1000 0000 + 0111 1111结果为1111 1111,十进制为-1,我们可以找镜像,即把这个负数变成对应的正数(1变0,0变1),比如1011 0110,镜像过去的正数是0100 1001,用-1减去这个正数得到的就是负数,即-1-(1+8+64)=-74。
这种方法与原反补转换计算有符号数几乎相同,但我们可以找捷径计算,直接拿到有符号数1011 0110,按照位权相加,此时要看有0的位置,即1+8+64=73,最后加1再取负数,即-(73+1)=-74,得到的就是有符号数的十进制大小。
第四种说白了就是既然转成原码后可以按照“1”位置的位权相加,那么如果不转成原码呢?我们就可以看“0”位置的位权相加(反正一个负数的二进制最高位是1,看“0”的位权没有影响)。之后转成原码要加1,那我们不转也要加1(是不是有同学想成减1了,其实这时候不加1的值和原码不加1计算出来的值是一样的),最后整体加个负数即可。听懂掌声~