移码及浮点数在内存中的存储方式
原博客搬移到:https://blog.csdn.net/u013171226/article/details/107680361
First of all,we need to know how the decimal decimal is converted into a binary decimal.计算机根本就不认识10进制的数据,他只认识0和1,所以,10进制的小数在计算机中是用二进制的小数表示的。
十进制的小数转化为二进制的小数的方法:
可以简单的概括为正序取整,将十进制的小数部分乘以2,然后取整数部分。
例如将0.2转化为二进制的小数,那么0.2*2=0.4,其整数部分是0,所以二进制小数的第一位为0,然后0.4*2=0.8,其整数部分是0,所以二进制小数的第二位为0,然后0.8*2=1.6,其整数部分是1,所以二进制小数的第三位是1,然后0.6*2=1.2,其整数部分是1,所以二进制小数的第四位是1。就这样一直计算下去,
再例如8.25用二进制怎么表示:首先8用二进制表示是1000,然后0.25*2=0.5,其整数部分是0,所以二进制小数的第一位是0.然后0.5*2=1.0,所以二进制小数的第二位是1,所以8.25用二进制表示就是1000.01.
移码:已知一个10进制数,怎么求它所对应的移码,补码的符号位取反就是移码,或者是加上128(假设是8位),可以这么理解,但是这样说应该是不对的,补码应该是没有符号位的,确切的说应该是,补码的最高位取反就是移码,或者是加上128.
例如,要求1的移码,1+128=129,,129=1000 0001或者用先求补码然后符号位取反的方法,那么先求出1的补码,1的补码是0000 0001,然后把他的符号位取反得到1000 0001,可以看到,两种方法求得的结果是一样的,
再比如求-1的移码,-1+128=127=0111 1111,或者可以先求出-1的补码,-1的补码是1111 1111,那么把符号位取反得到移码0111 1111,两种方法得到的结果是一样的。
另外,这里求解移码的方法和下面浮点数存储的时候求移码(阶码)的方法是不一样的,百度输入移码,上面一般会说,移码一般用作浮点数的阶码,但是这里说的移码的求法是补码加上128或者补码的符号位取反,而下面求浮点数阶码的时候是补码加上127。
另外,为什么要用移码表示阶码,而不用补码表示阶码,采用这种方式表示的目的是简化比较。因为,指数必须是有符号数才能表达很大或很小的数值,如果采用补码表示的话,首先小数是有符号的,如果指数也是有符号的,那么就不能简单的对浮点数进行大小比较。因为根据补码比较大小的话,要先转换成原码再比较大小,正因为如此,指数部分使用所谓的偏正值形式表示,实际值为原始值与一个固定值(8位的情况是127)的和。将它的值调整到一个无符号数的范围内以便进行比较。因为移码没有符号位,所以我们直接可以由移码的表示形式看出对应数值的大小,由此可见,移码是没有符号位的,移码的最高位不能看做是符号位而应看做是数值位,例如上面的1000 0001把最高位看成数值位才得到129。
C语言浮点数存储方式
浮点数(单精度float和双精度的double)在内存中是以二进制的科学计数法表示的, ,主要由三部分构成:符号位+阶码+尾数。float存储时使用4个字节,double存储时使用8个字节。各部分占用位宽如下所示:
符号位 阶码 尾数 长度
float 1 8 23 32
double 1 11 52 64
符号位:0表示正数,1表示负数,注意这里的符号位是尾数的符号不是指数部分的符号,注意,所有的浮点数都是有符号数,不能定义unsigned float,这样定义的话,编译器会报错,当然有的编译器只是警告而不报错,
阶码(指数部分):用于存储科学计数法中的指数数据,并且采用移位存储,
尾数部分:尾数部分下面会详细说明,
其中float的存储方式如下图所示:
而双精度的存储方式为:
关于尾数部分需要说明一下:注意,尾数用的是原码,8.25用二进制表示可表示为1000.01,用二进制的科学计数法可以表示为1.00001*,120.5用二进制表示1111000.1用二进制的科学计数法可以表示为1.1110001*,任何一个数的科学计数法表示都为1.xxx*,因为尾数部分小数点前面都是1,所以可以将小数点前面的1省略,尾数部分就可以表示为xxxx,例如,0.5的二进制形式为0.1,由于规定正数部分必须为1,将小数点右移1位,则为1.0*2^(-1),而尾数1.0去掉整数部分为0,补齐0到23位00000000000000000000000,则其尾数部分就是 00000000000000000000000,由于将前面的1都省略了,所以23bit的尾数部分,可以表示的精度却变成了24bit,道理就是在这里,那24bit能精确到小数点后几位呢,我们知道9的二进制表示为1001,所以4bit能精确十进制中的1位小数点,24bit就能使float能精确到小数点后6位,
关于阶码的求法:例如对于3,想求3对应的阶码是多少,方法是补码加上127,3+127=130=1000 0010(注意最高位也是数值位),再例如求6所对应的阶码,6+127=133=1000 0101,
已知阶码,想求出原始的数据,方法是阶码减去127,例如1000 0010=130,然后130减去127=3,再例如1000 0101=133,然后133-127=6,
下面举例说明浮点数在内存中的存储方式,知道了尾数的省略1的规定,知道了阶码的求法,接下来就可以看两个浮点数存储的例子了,例如8.25,首先转换为二进制的小数是1000.01=1.00001*2^3,首先确定符号位,8.25是正数因此符号位是0,然后求阶码,3的补码加上127的补码=1000 0010.然后是尾数部分,尾数的表示:去掉小数点前面的1,为00001,后面补充0至23位:000 0100 0000 0000 0000 0000最终8.25在内存里存储的二进制为:0100 0001 0000 0100 0000 0000 0000 0000,
下面说一下,浮点数存储的时候几个特殊值的问题,
1:如果指数部分不全为0并且不全为1:也就是0<指数部分<255,这种情况称为正规形式,这个时候浮点数就采用上面的介绍的规则计算,这个时候指数E就等于阶码减去127,求小数的时候在小数部分前面添加1,即1.xxxx。
2:如果指数部分E全是0:这时浮点数的指数E等于1-127(而不是0-127,这是规定),求小数的时候不再加上第一位的1,而是还原为0.xxxxxx的小数,这样做是为了表示±0,以及接近于0的很小的数字, 关于0,IEEE规定0.0是一个特殊的数,阶码和尾数全为零来表示浮点的零。
3:如果指数部分E全为1:这个时候如果尾数部分全是0,表示 ± ∞(正负取决于符号位S),如果尾数部分不全为0,表示这个数不是一个数(NaN,not a number)。
指数的取值范围是多少:通过上面对于常规形式和非常规形式的分析,那么现在来看一下浮点数的指数取值范围是多少,指数部分是一个无符号整数,这意味着,对于float类型,由于指数部分是8位,因此它的取值范围是0到255,当阶码是0的时候,根据上面的规定,这个时候指数是1-127=-126,而当阶码等于255的时候又是特殊值,所以不能算,当阶码是254的时候,指数等于254-127=127,因此可以不恰当的说指数的取值范围是-126到127.
关于float型变量所能表示的数的范围的问题:先来分析一下float类型所能表示的绝对值最大的数,上面分析了指数的最大值是127,而尾数部分的最大值就是23个1,这个时候就是float所能表示的最大值,这个值是1.111 1111 1111 1111 1111 1111*2^127=3.4*10^38.当符号位是1时表示负数-3.4*10^38。
float所能表示的绝对值最小的数是多少呢,当指数部分全是0的时候,这个时候指数=1-127=-126,然后尾数部分的最小值000 0000 0000 0000 0000 00001,这个时候小数是0.000 0000 0000 0000 0000 0001.因此浮点数所能表示的绝对值最小的数应该是1*2^-149 。(这个不太确定,好像是求错了,)
关于浮点数的上溢和下溢: 在C Primer Plus的第三章后面有一个编程练习题,“通过试验的方法,观察系统如何处理整数上溢,浮点数上溢,浮点数下溢的情况”参考答案是这样的;
#include<stdio.h>
int main(void)
{
unsigned int a=4294967295;
float b=3.4E38;
float c=b*10;
float d=0.1234E-2;
printf("%u+1=%u\n",a,a+1);
printf("%e*10=%e\n",b,c);
printf("%f/10=%f\n",d,d/10);
return(0);
}
/*
在VC++6.0中的输出结果为:
4294967295+1=0
3.400000e+038*10=1.#INF00e+000
0.001234/10=0.000123 丢失了一个有效数字
Press any key to continue
*/
刚开始的时候不理解,0.001234/10=0.0001234,那么不就是1.234E-4吗,浮点数完全可以存储这个数,怎么就会丢失有效位了呢,再仔细看了一下才发现,程序里面的printf函数用的是%f输出,%f是普通的十进制输出,并不是科学计数法输出,因此会丢失有效位,而如果把%f换成%e或者%E,那么0.001234除以10之后是不会丢失有效位的,
再例如:
#include<stdio.h>
int main(void)
{
int a = 0x00000009;
float b;
b = (float)a;
printf("%e\n",a);
return 0;
}
如果用%f输出的话,那么得到的结果将是0.000000.因为将0x00000009拆分,得到第一位符号位s=0,后面8位的指数E=00000000,最后23位的有效数字M=000 0000 0000 0000 0000 1001。由于指数E全为0,所以求指数的时候是1-127=-126,而求小数部分的时候前面不是添加1而是添加零,因此,浮点数V就写成:V=(-1)^0×0.00000000000000000001001×2^(-126)=1.001×2^(-146),这个时候用%f输出的话,得到的结果就是0了,
浮点数存储的时候存在误差的问题:举例说明,2.2,将十进制的小数转换为二进制的小数的方法是:将小数*2,取整数部分。
0.2×2=0.4,所以二进制小数第一位为0.4的整数部分0;
0.4×2=0.8,第二位为0.8的整数部分0;
0.8×2=1.6,第三位为1;
0.6×2=1.2,第四位为1;
0.2×2=0.4,第五位为0;
...... 这样永远也不可能乘到=1.0,得到的二进制是一个无限循环的排列 00110011001100110011...
对于单精度数据来说,尾数只能表示24bit的精度,所以2.2的 float存储为:
但是这种存储方式,换算成十进制的值,却不会是2.2。因为在十进制转换为二进制的时候可能会不准确,这样就导致了误差问题!所以在浮点数表示中,某些数在存储的时候就会存在误差,而对于有些数据(如2.25),在将十进制转换为二进制表示的时候恰好能够计算完毕,所以这个误差就不会存在。
怎么比较两个浮点数的大小:很多C程序员的笔试会有浮点数比较大小的题目,因为浮点型只是一个近似值,也就是一个值可能表示一个范围区间,这样的表达方式就使得对浮点型数据采用做差判断是否等于0的方法进行比较可能不合理,只有通过比较一个数是否在这个小的范围内,因此在计算值比较两个浮点数变量不能通过做差是否等于零来判断。而只能通过如下的方式判断:
const float ESPSION = 0.000001;
if((x-y)>=-0.000001&& (x-y)<=0.000001)
这种实现方式是基本的比较方式,这种判断方法刚好就是判断变量是否处于一个范围内,这里的范围是-0.000001<x<0.000001。判断一个值是否为0的方式用大于-0.000001小于0.000001来判断,这样浮点的0是一个很接近于0数,但不是0,这样就不会引发除0错误,0.0其实不是0,当x落在了±0.000001之内,x + 1.0 = 1.0,就是这么规定的。x在此范围之内的话,都被计算机认为是0.0 。
在看C Primer Plus的时候,有一个课后题是关于浮点数的上溢和下溢的问题,看了答案之后感觉不明白,于是去查一下浮点数的存储问题,这一查才发现浮点数的存储牵扯到的东西还挺多的,由于智商平平,浮点数的存储这个小问题前前后后竟然耽误了10天左右的时间,看了一个又一个的博客,最后才算对浮点数的存储稍微了解了,下面是自己做的笔记,部分内容是复制的别人的博客,无意侵权,纯粹是为了做笔记。
笔记中如有错误恳。各位前辈指正,好让我及时改正,以免我把错误的东西当成正确的了,
作者:cumtchw
出处:http://www.cnblogs.com/cumtchw/
我的博客就是我的学习笔记,学习过程中看到好的博客也会转载过来,若有侵权,与我联系,我会及时删除。