float,double

    技术2022-07-12  74

    一,浮点数是怎么存储的

    1,单精度浮点数如下

    第1位表示正负,中间8位表示指数,后23位储存有效数位(有效数位是24位)。

    第一位的正负号0代表正,1代表负。

    中间八位共可表示28=256个数,指数可以是二进制补码或0到255,0到126代表-127到-1,127代表零,128-255代表1-128。

    有效数位最左手边的1并不会储存,因为它一定存在(二进制的第一个有效数字必定是1)。换言之,有效数位是24位,实际储存23位。

     

     

     

     2,为什么不这么存呢??

    sign存正负,exponent存小数点位置,fraction存去掉小数点之后的数字值。

        如果这么存,范围不是连续的。能存0.111,能存0.112,但是不能存0.1111(假如能存3位数字)。第二,两个float32的值计算之后的值可能无法存在float32中。比如0.111*0.111=0.012321.

        实际上现在的单精度浮点数,exponent存的就是二进制下,“小数点”的位置,例如上面例子中exponent=-3,二进制下“小数点”后有三位,实际的值要乘以2-3次方。一个整数转换为float的话,会表示成科学计数法,由小数(精度)和指数构成,对0,1四舍五入。

    3,double

    种类-------符号位-------------指数位----------------尾数位---- float---第31位(占1bit)---第30-23位(占8bit)----第22-0位(占23bit) double--第63位(占1bit)---第62-52位(占11bit)---第51-0位(占52bit)

    取值范围主要看指数部分: float的指数部分有8bit(2^8),由于是有符号型,所以得到对应的指数范围-128~127。 double的指数部分有11bit(2^11),由于是有符号型,所以得到对应的指数范围-1024~1023。

    由于float的指数部分对应的指数范围为-128~127,所以取值范围为: -2^128到2^127,约等于-3.4E38 — +3.4E38

    精度(有效数字)主要看尾数位: float的尾数位是23bit,对应7~8位十进制数,所以有效数字有的编译器是7位,也有的是8位。

     

    二,float 和int转换

        那么真正存在内存里的这个二进制数,转化回十进制,到底是比原先的十进制数大呢,还是小呢?答案是It depends。人计算十进制的时候,是四舍五入,计算机再计算二进制小数也挺简单,就是0舍1入。对于float,要截断成为23位,假如卡在24位上的是1,那么就会造成进位,这样的话,存起来的值就比真正的十进制值大了,如果是0,就舍去,那么存起来的值就比真正的十进制值小了。因此,这可以合理的解释一个问题,就是0.6d转换成float再转换回double,它的值是0.60000002384185791,这个值是比0.6大的,原因就是 0.6的二进制科学计数法表示,第24位是1,造成了进位。

    int,又称作整型,在.net中特指的是Int32,为32位长度的有符号整型变量。 

    float,单精度浮点数,32位长度,1位符号位,8位指数位与23位数据位,在.net中又称为Single(.net core中定义就是system.single)。

    double,64位长度的双精度浮点数,1位符号位,11位指数位,52位数据位。

        它们互相的关系就是:int可以稳式转换成float和double,float只能强制转换成int,但是可以隐式转换成double,double只能强制转换成float和int。

        温习一下计算机组成原理时学习到的一些知识,就是二进制补码表示以及浮点数表示。我想把一个十进制转化为二进制的方法已经不用多费唇舌,只不过为了计算方便以及消除正零与负零的问题,现代计算机技术,内存里存的都是二进制的补码形式,当然这个也没什么特别的,只不过有某些离散和点,需要特殊定义而已,比如-(2^31),这个数在int的补码里表示成1000…(31个零),这个生套补码计算公式并不能得到结果(其实不考虑进位的话还真是这个结果,但是总让人感觉很怪)。再者,浮点数,其实就是把任何二进制数化成以0.1....开头的科学计数法表示而已。

    废话说完,这就出现了几个问题,而且是比较有意思的问题。

     int i = Int32.MaxValue;

     float f = i;

     int j = (int)f;

     bool b = 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这个全集下并不连续。

     double d = 0.6;

     float f = (float)d;

     double d2 = f;

     bool b = d == d2;

        这里的b,也是false。刚才这个操作,如果开始另d等于0.5,结果就将会是true。乍一看,0.6这个数这么短,double和float都肯定能够表示,那么转换过去再转换回来,结果理应相等。其实这是因为我们用十进制思考问题太久了,如果我们0.6化成二进制小数,可以发现得到的结果是0.10011001……(1001循环)。这是一个无限循环小数。因此,不管float还是double,它在存储0.6 的时候,都无法完全保存它精确的值(计算机不懂分数,呵呵),这样的话由于float保存23位,而double保存52位,就造成了double转化成 float的时候,丢失掉了一定的数据,非再转换回去的时候,那些丢掉的值被补成了0,因此这个后来的double和从前的double值已经不再一样了。

        这样就又产生了一个问题,什么样的double转换成float再转换回来,两个的值相等呢?其实这个问题与刚才int的那个问题惊人的相似(废话,都和float打交道,能不相似么),只不过我们还需要考虑double比float多了3位的指数位,太大的数double能表示但float 不行。

        还有一个算是数学上的问题,什么样的十进制小数,表示成二进制不是无限小数呢?

     

    实测

    C# (.net core)

    0.59999999f   调试显示0.599999964

    0.599999999f 调试显示0.6

    Golang(1.13.4)

    0.59999999     调试显示0.59999996

    0.599999999   调试显示0.6

    Processed: 0.009, SQL: 9