1.数据存储
float的指数位有8位,而double的指数位有11位,分布如下:
float:
1bit(符号位) | 8bits(指数位) | 23bits(尾数位) |
double:
1bit(符号位) | 11bits(指数位) | 52bits(尾数位) |
在数学中,特别是在计算机相关的数字(浮点数)问题的表述中,有一个基本表达法:
valueoffloating-point= significandx base^exponent,withsign
即:数值 = 尾数 ×底数^指数,(附加正负号)
其实不论是float还是double在存储方式上都是遵从IEEE的规范的,float遵从的是IEEER32.24,而double遵从的是R64.53。R32. 24和R64.53的存储方式都是用科学计数法来存储数据的。
指数部分的存储采用移位存储,存储的数据为元数据+127,下面就看看8.25和120.5在内存中真正的存储方式。
8.25用十进制的科学计数法表示就为:8.25*10^0,而120.5可以表示为:1.205*10^2。而在计算机存储中,首先要将上面的数更改为二进制的科学计数法表示,8.25用二进制表示可表示为1000.01,120.5用二进制表示为:1110110.1用二进制的科学计数法表示1000.01可以表示为1.0001*2^3,1110110.1可以表示为1.1101101*2^6。
首先看下8.25,二进制:1.0001*2^3
按照上面的存储方式,符号位为:0,表示为正,指数位为:3+127=130,位数部分为,故8.25的存储方式如下图所示:
而单精度浮点数120.5的存储方式如下图所示:
对于一个小数其默认类型为double型,如果需要指定为float型,需要在小数后面添加F或f后缀。
如floata=0.5f;
2.范围
范围主要是来讨论最大最小值。
float和double的范围是由指数的位数来决定的。对于指数部分,因为指数可正可负,8位的指数位能表示的指数范围就应该为:-127~+128了。
即float的指数范围为-127 ~+128(+-2^7),
而double的指数范围为-1023 ~ +1024 (+-2^10),
并且指数位是按补码的形式来划分的。
其中负指数决定了浮点数所能表达的绝对值最小的非零数;而正指数决定了浮点数所能表达的绝对值最大的数,也即决定了浮点数的取值范围。
float的范围为-2^128 ~+2^128 也即-3.40E+38~+3.40E+38;
double的范围为-2^1024~+2^1024,也即-1.79E+308~+1.79E+308。
类型名 | 占内存字节数(B) | 值的范围 |
float | 4 | -3.40E+38~+3.40E+38 |
double | 8 | -1.79E+308~+1.79E+308 |
longdouble | 10 | -3.4E+4932~+1.1E+4932 |
其他特殊表示
111111111尾数的23位不全为0:表示不是数值
11111111100000000000000000000000:表示负无穷大.比最小负数还小的数都视为负无穷大.
11111111011111111111111111111111:表示最小的负数,即-2^128=-3.4*10^38
10000000100000000000000000000000:表示最大的负数,即-2^(-126)=-1.18*10^(-38)
00000000000000000000000000000000:表示0
00000000100000000000000000000000:表示最小的正数,即2^(-126)=1.18*10^(-38)
01111111011111111111111111111111:表示最大的正数,即2^(128)=3.4*10^(38)
01111111100000000000000000000000:表示正无穷大,比最大正数还大的数都视为正无穷大.
011111111尾数的23位不全为0:表示不是数值
当指数部分和小数部分全为0时,表示0值,有+0和-0之分(符号位决定),0x00000000表示正0,0x80000000表示负0.
指数部分全1,小数部分全0时,表示无穷大,有正无穷和负无穷,0x7f800000表示正无穷,0xff800000表示负无穷.
故,float型数据的表示范围包括:
负无穷大;-3.4*10^38<x<-1.175*10^(-38);0;1.175*10^(-38)<x<3.4*10^38;正无穷大.
3.精度
精度主要是来讨论数据通过科学计数法表达后,小数点后数据位数。
float和double的精度是由尾数的位数来决定的。任何一个数的科学计数法表示都为1.xxx*2^n,尾数部分就可以表示为xxxx,第一位都是1,可以将小数点前面的1省略,故不能对精度造成影响。
那么对于尾数
float:2^23=8388608,一共七位,这意味着最多能有7位有效数字,但绝对能保证的为6位,也即float的精度为6~7位有效数字;
double:2^52=4503599627370496,一共16位,同理,double的精度为15~16位。
或,
log(2^23)=6.xx,取整即6;
log(2^53)=15.95,取整即15;
log(2^65)=19.57,取整即19。
4.问题
inti=Int32.MaxValue;
floatf=i;
intj=(int)f;
boolb=i==j;
这里的b,是false。刚才这个操作,如果我们把float换成long,第一次进行隐式转换,第二次进行强制转换,结果将会是true。乍一看,float.MaxValue是比int.MaxValue大了不知道多少倍的,然而这个隐式转换中,却造成了数据丢失。int.MaxValue,这个值等于2^31-1,写成二进制补码形式就是01111…(31个1),这个数,在表示成float计数的科学计数法的时候,将会写成+0.1111…(23个1)*2^31,对于那31个1,里面的最后8个,被float抛弃了,因此,再将这个float强制转换回int的时候,对应的int的二进制补码表示已经变成了0111…(23个1)00000000,这个数与最初的那个int相差了255,所以造成了不相等。
那么提出另一个问题,什么样的int变成float再变回来,和从前的值相等呢?这个问题其实完全出在那23位float的数据位上了。对于一个int,把它写成二进制形式之后,成为了个一32个长度的0、1的排列,对于这个排列,只要第一个1与最后一个1之前的间距,不超过23,那么它转换成float再转换回来,两个值就会相等。这个问题是与大小无关的,而且这个集合在int这个全集下并不连续。
floatf=2.2f;
doubled=(double)f;
Console.WriteLine(d.ToString("0.0000000000000"));
f=2.25f;
d=(double)f;
Console.WriteLine(d.ToString("0.0000000000000"));
可能输出的结果让大家疑惑不解,单精度的2.2转换为双精度后,精确到小数点后13位后变为了2.2000000476837,而单精度的2.25转换为双精度后,变为了2.2500000000000,为何2.2在转换后的数值更改了而2.25却没有更改呢?首先我们看看2.25的单精度存储方式,01000000100100000000000000000000,而2.25的双精度表示为:0100000000010010000000000000000000000000000000000000000000000000,这样2.25在进行强制转换的时候,数值是不会变的,而我们再看看2.2呢,2.2用科学计数法表示应该为:将十进制的小数转换为二进制的小数的方法为将小数*2,取整数部分,所以0.282=0.4,所以二进制小数第一位为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,这样永远也不可能乘到=1.0,得到的二进制是一个无限循环的排列00110011001100110011...,对于单精度数据来说,尾数只能表示24bit的精度,所以2.2的float存储为
但是这样存储方式,换算成十进制的值,却不会是2.2的,应为十进制在转换为二进制的时候可能会不准确,如2.2,而double类型的数据也存在同样的问题,所以在浮点数表示中会产生些许的误差,在单精度转换为双精度的时候,也会存在误差的问题,对于能够用二进制表示的十进制数据,如2.25,这个误差就会不存在,所以会出现上面比较奇怪的输出结果。
类似地,
doubled=0.6;
floatf=(float)d;
doubled2=f;
boolb=d==d2;
这里的b,也是false。由于float保存23位,而double保存52位,就造成了double转化成float的时候,丢失掉了一定的数据,非再转换回去的时候,那些丢掉的值被补成了0,因此这个后来的double和从前的double值已经不再一样了。
那么什么样的十进制小数,表示成二进制不是无限小数呢?对于所有的最后一位以5结尾的十进制有限小数,都可以化成二进制的有限小数。
浮点数与十进制之间的互相转换体现在存在内存里的数明显实际与0.6不等,但是无论哪种语言,都能够在Debug以及输入的时候,将它正确的显示成0.6提供给用户(程序员),最好的例子就是double和ToString方法,如果我写doubled=0.59999999999999999999999999999,d.ToString()给我的是0.6。
最后,一个有意思有问题,刚才说过0.6表示成为二进制小数之后,是0.1001并且以1001为循环节的无限循环小数,那么在我们将它存成浮点数的时候,一定会在某个位置将它截断(比如float的23位和double的52位),那么真正存在内存里的这个二进制数,转化回十进制,到底是比原先的十进制数大呢,还是小呢?答案是Itdepends。人计算十进制的时候,是四舍五入,计算机再计算二进制小数也挺简单,就是0舍1入。对于float,要截断成为23位,假如卡在24位上的是1,那么就会造成进位,这样的话,存起来的值就比真正的十进制值大了,如果是0,就舍去,那么存起来的值就比真正的十进制值小了。因此,这可以合理的解释一个问题,就是0.6d转换成float再转换回double,它的值是0.60000002384185791,这个值是比0.6大的,原因就是0.6的二进制科学计数法表示,第24位是1,造成了进位。