• C语言中float,double等类型,在内存中的结构


    从存储结构和算法上来讲,double和float是一样的,不一样的地方仅仅是float是32位的,double是64位的,所以double能存储更  
       
      高的精度。  
       
              任何数据在内存中都是以二进制(0或1)顺序存储的,每一个1或0被称为1位,而在x86CPU上一个字节是8位。比如一个16位(2  
       
      字节)的short   int型变量的值是1000,那么它的二进制表达就是:00000011   11101000。由于Intel   CPU的架构原因,它是按字节倒  
       
      序存储的,那么就因该是这样:11101000   00000011,这就是定点数1000在内存中的结构。  
       
              目前C/C++编译器标准都遵照IEEE制定的浮点数表示法来进行float,double运算。这种结构是一种科学计数法,用符号、指数和  
       
      尾数来表示,底数定为2——即把一个浮点数表示为尾数乘以2的指数次方再添上符号。下面是具体的规格:  
       
      ````````符号位   阶码   尾数   长度  
      float         1           8         23       32  
      double       1         11         52       64  
      临时数       1         15         64       80  
       
      由于通常C编译器默认浮点数是double型的,下面以double为例:  
      共计64位,折合8字节。由最高到最低位分别是第63、62、61、……、0位:  
              最高位63位是符号位,1表示该数为负,0正;  
              62-52位,一共11位是指数位;  
              51-0位,一共52位是尾数位。  
       
       
              按照IEEE浮点数表示法,下面将把double型浮点数38414.4转换为十六进制代码。  
              把整数部和小数部分开处理:整数部直接化十六进制:960E。小数的处理:  
      0.4=0.5*0+0.25*1+0.125*1+0.0625*0+……  
              实际上这永远算不完!这就是著名的浮点数精度问题。所以直到加上前面的整数部分算够53位就行了(隐藏位技术:最高位的1  
       
      不写入内存)。  
              如果你够耐心,手工算到53位那么因该是:38414.4(10)=1001011000001110.0110101010101010101010101010101010101(2)  
      科学记数法为:1.001……乘以2的15次方。指数为15!  
              于是来看阶码,一共11位,可以表示范围是-1024   ~   1023。因为指数可以为负,为了便于计算,规定都先加上1023,在这里,  
       
      15+1023=1038。二进制表示为:100   00001110  
              符号位:正——   0   !  
              合在一起(尾数二进制最高位的1不要):  
      01000000   11100010   11000001   11001101   01010101   01010101   01010101   01010101  
              按字节倒序存储的十六进制数就是:  
      55   55   55   55   CD   C1   E2   40

    float存储格式及FPU  

    浮点数用科学计数法的形式存储, 即分成符号位, 底数位和指数位
    如 10.0 的二进制表示为 1010.0, 科学表示法表示为: 1.01exp110, 即 (1+0*1/2+1*1/4)*2^3. 小数点每左移一位指数要加1, 每右移一位指数要减1.

    其存储格式符合IEEE标准, 即

    数据格式        符号位     底数位     指数位


    单精度 float     1        23          8


    双精度 double    1        52         11


    扩展精度         1        64         15

    存储顺序为: 符号位 指数位 底数位, 由于底数的个位必为1, 因此个位和小数点就不存储. 指数位的高位也是符号位, 不过为1表示正, 0表示负.

    float 因有 8 bits, 所以能表示的有 2 的 256 次方,
    但因為指數應可正可負,
    所以 IEEE 規定, 此處算出的次方須減去 127 才是真的指數,
    所以 float 的指數可從-126 到 128.
    double 型態有 11 bits, 算出的值須減去 1023,
    所以double 的指數可從 -1022 到 1024.
    底数   (mantissa):      
    〈特例〉0   不能以 2 的次方表示
    float : 00 00 00 00
    double: 00 00 00 00 00 00 00 00

    由此可推断浮点数的精度. 单精度的底数只存储23位, 即最小一位为 1/2^23, 故精度为 1/2^23 = 1.19209e-7, 可精确到小数点后6位; 双精度的底数存储52位, 最小位为 1/2^52, 精度为 1/2^52 = 2.22045e-16, 能精确到小数点后15位.

    双精度的最大值约为 2*2^(2^10-1), 约为1.79e308


    关于FPU:
    (gdb) list 1,80
    1        #include <stdio.h>
    2        int main(int argc,char **argv)
    3        {
    4        float val = 1000;
    5        int   ival = val;
    6        printf("%f/n",val);
    7        printf("%d/n",ival);
    8        }
    (gdb) display/i $pc
    (gdb) break main
    Breakpoint 1 at 0x8048365: file float.c, line 4.
    (gdb) r
    Starting program: /home/lsm1982/float

    Breakpoint 1, main () at float.c:4
    4        float val = 1000;
    1: x/i $pc   0x8048365 <main+17>:         mov     $0x447a0000,%eax
    (gdb) si
    0x0804836a       4        float val = 1000;
    1: x/i $pc   0x804836a <main+22>:         mov     %eax,0xfffffff4(%ebp)
    (gdb)
    5        int   ival = val;
    1: x/i $pc   0x804836d <main+25>:         flds    0xfffffff4(%ebp)
    (gdb)
    0x08048370       5        int   ival = val;
    1: x/i $pc   0x8048370 <main+28>:         fnstcw 0xffffffea(%ebp)
    (gdb)
    0x08048373       5        int   ival = val;
    1: x/i $pc   0x8048373 <main+31>:         movzwl 0xffffffea(%ebp),%eax
    (gdb)
    0x08048377       5        int   ival = val;
    1: x/i $pc   0x8048377 <main+35>:         mov     $0xc,%ah
    (gdb)
    0x08048379       5        int   ival = val;
    1: x/i $pc   0x8048379 <main+37>:         mov     %ax,0xffffffe8(%ebp)
    (gdb)
    0x0804837d       5        int   ival = val;
    1: x/i $pc   0x804837d <main+41>:         fldcw   0xffffffe8(%ebp)
    (gdb)
    0x08048380       5        int   ival = val;
    1: x/i $pc   0x8048380 <main+44>:         fistpl 0xfffffff8(%ebp)
    (gdb)
    0x08048383       5        int   ival = val;
    1: x/i $pc   0x8048383 <main+47>:         fldcw   0xffffffea(%ebp)
    (gdb)
    6        printf("%f/n",val);
    1: x/i $pc   0x8048386 <main+50>:         flds    0xfffffff4(%ebp)
    看来强制类型转换并不只是将存储的
    的二进制代码进行不同类型的解释,还有其他可能的情况。
    FPU也有其在处理器中的特殊性
    5的二进制是101,即1.01(二进制)×2^2 [所有相关帖子]

    Float 格式数据长32 bits,最高位为符号位:0为正,1为负;紧接着的8位为阶码:为了便于比较大小,其固定偏移7FH长,即0实际表示-7FH,7FH实际表示 0,0FFH实际表示80H;余下的低23位为尾数(有效数字),为了使有效数字达到最大精度,这23个有效数字隐含着固定位1[注2],比如尾数 10000000000000000000001其实就是1.10000000000000000000001,1被省略,而小数点固定在首位。

    符号位 0
    阶码 0x7F + 2 = 0x81,即二进制的 10000001
    尾数 01000000000000000000000 (应该是1.01000000000000000000000,其中1.是缺省具有的,上面说过了)
    合起来就是 0 10000001 01000000000000000000000,即16进制的 40A00000

     0x7F + 2 = 0x81 为什么加2? <无内容> - [bl] 2009-5-7 10:22:50 ( 0 字节, 点击:2 )

     2的二次方嘛,+2 就是指“二次方” <无内容> - [周星星] 2009-5-7 10:25:36 ( 0 字节, 点击:2 )

     2的二次方 怎么来的? <无内容> - [bl] 2009-5-7 10:26:35 ( 0 字节, 点击:1 )



     to bl:《科学计数法》503表示为5.03E+02,0.7表示为7.0E-01,7表示为7.0E+00 <无内容> - [周星星] 2009-5-7 10:27:41 ( 0 字节, 点击:3 )

     上学的时候学过,现在全都忘光:) <无内容> - [jbisjb] 2009-5-7 10:31:17 ( 0 字节, 点击:0 )

     同样,二进制的101用科学计数法表示就是1.01E+02 <无内容> - [周星星] 2009-5-7 10:28:16 ( 0 字节, 点击:2 )

     同时,因为二进制只有0和1,而科学计数法第一位都是非0,即二进制的科学计数法第一位都是1.,既然都是1.,那么自然可以省略掉 <无内容> - [周星星] 2009-5-7 10:29:34 ( 0 字节, 点击:2 )

     因为省略了1.,所以float/double其实是无法表示0.0的,这时候…… <无内容> - [周星星] 2009-5-7 10:30:33 ( 0 字节, 点击:4 )

     这时候就用 1.0E负最大 来表示 +0.0,用 -1.0E负最大 来表示 -0.0 <无内容> - [周星星] 2009-5-7 10:31:50 ( 0 字节, 点击:2 )

     恩恩,听起来好像有什么复杂的隐情哦 

     <无内容> - [bl] 2009-5-7 10:32:28 ( 0 字节, 点击:0 ) 

    语言中,对于浮点类型的数据采用单精度类型(float)和双精度类型(double)来存储,float数据占用32bit,double数据占用64bit,我们在声明一个变量float f= 2.25f的时候,是如何分配内存的呢?如果胡乱分配,那世界岂不是乱套了么,其实不论是float还是double在存储方式上都是遵从IEEE的规范的,float遵从的是IEEE R32.24 ,而double 遵从的是R64.53。

        无论是单精度还是双精度在存储中都分为三个部分:

    1. 符号位(Sign) : 0代表正,1代表为负
    2. 指数位(Exponent):用于存储科学计数法中的指数数据,并且采用移位存储(后面将详细说明)
    3. 尾数部分(Mantissa):尾数部分

     其中float的存储方式如下图所示:

    而双精度的存储方式为:

        R32.24和R64.53的存储方式都是用科学计数法来存储数据的,比如8.25用十进制的科学计数法表示就为:8.25*clip_image0021,而120.5可以表示为:1.205*clip_image0022, 这些小学的知识就不用多说了吧。而我们傻蛋计算机根本不认识十进制的数据,他只认识0,1,所以在计算机存储中,首先要将上面的数更改为二进制的科学计数 法表示,8.25用二进制表示可表示为1000.01,我靠,不会连这都不会转换吧?那我估计要没辙了。120.5用二进制表示为:1110110.1用 二进制的科学计数法表示1000.01可以表示为1.0001*clip_image002[2],1110110.1可以表示为1.1101101*clip_image002[3],任何一个数都的科学计数法表示都为1.xxx*clip_image002[1], 尾数部分就可以表示为xxxx,第一位都是1嘛,干嘛还要表示呀?可以将小数点前面的1省略,所以23bit的尾数部分,可以表示的精度却变成了 24bit,道理就是在这里,那24bit能精确到小数点后几位呢,我们知道9的二进制表示为1001,所以4bit能精确十进制中的1位小数 点,24bit就能使float能精确到小数点后6位,而对于指数部分,因为指数可正可负,8位的指数位能表示的指数范围就应该为:-127-128了, 所以指数部分的存储采用移位存储,存储的数据为元数据+127。

    例如,我们要想偷窥浮点类型的值4.25在计算机硬盘中存储的庐山真面目,请跟我来:首先把4.25转换成二进制的表达方式,即100.01,在详细点,变成1.0001x22,好了,对号入座把。

    Sign=0;

    Exponent(bias)=2+127=129 (偏移量为127,就是直接加上个127了);

    Mantissa=1.0001-1.0=0001(规格化后,小数点前总是整数1,全世界人都知道前面是1不是0,所以省略不写了,即尾数部分不包括整数部分;当别人问你,为什么23 bit的尾数部分可以表示24位的精度,知道怎么回答了吧。

     

    下面就看看8.25和120.5在内存中真正的存储方式。

         首先看下8.25,用二进制的科学计数法表示为:1.0001*clip_image002[2]

    按照上面的存储方式,符号位为:0,表示为正,指数位为:3+127=130 ,位数部分为,故8.25的存储方式如下图所示:

    而单精度浮点数120.5的存储方式如下图所示:

    那么如果给出内存中一段数据,并且告诉你是单精度存储的话,你如何知道该数据的十进制数值呢?其实就是对上面的反推过程,比如给出如下内存数据:0100001011101101000000000000,首先我们现将该数据分段,0 10000 0101 110 1101 0000 0000 0000 0000,在内存中的存储就为下图所示:

    根据我们的计算方式,可以计算出,这样一组数据表示为:1.1101101*clip_image002[3]=120.5

    而双精度浮点数的存储和单精度的存储大同小异,不同的是指数部分和尾数部分的位数。所以这里不再详细的介绍双精度的存储方式了,只将120.5的最后存储方式图给出,大家可以仔细想想为何是这样子的

    文本框: 0     100 0000 0101    1101 1010 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000

    下面我就这个基础知识点来解决一个我们的一个疑惑,请看下面一段程序,注意观察输出结果

                float f = 2.2f;
    double d = (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的单精度存储方式,很简单 0 1000 0001 001 0000 0000 0000 0000 0000,而2.25的双精度表示为:0 100 0000 0001 0010 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000,这样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存储为:

    单精度数202的存储方式

    但是这样存储方式,换算成十进制的值,却不会是2.2的,应为十进制在转换为二进制的时候可能会不准确,如2.2,而double类型的数 据也存在同样的问题,所以在浮点数表示中会产生些许的误差,在单精度转换为双精度的时候,也会存在误差的问题,对于能够用二进制表示的十进制数据,如 2.25,这个误差就会不存在,所以会出现上面比较奇怪的输出结果。

    
    

    剖析float型的内存存储和精度丢失问题

    问题提出:12.0f-11.9f=0.10000038,"减不尽"为什么?

    现在我们就详细剖析一下浮点型运算为什么会造成精度丢失?

    1、小数的二进制表示问题

           首先我们要搞清楚下面两个问题:

         (1)  十进制整数如何转化为二进制数

               算法很简单。举个例子,11表示成二进制数:

                         11/2=5   余   1

                           5/2=2   余   1

                           2/2=1   余   0

                           1/2=0   余   1

                              0结束         11二进制表示为(从下往上):1011

              这里提一点:只要遇到除以后的结果为0了就结束了,大家想一想,所有的整数除以2是不是一定能够最终得到0。换句话说,所有的整数转变为二进制数的算法会不会无限循环下去呢?绝对不会,整数永远可以用二进制精确表示 ,但小数就不一定了。

          (2) 十进制小数如何转化为二进制数

               算法是乘以2直到没有了小数为止。举个例子,0.9表示成二进制数

                         0.9*2=1.8   取整数部分  1

                         0.8(1.8的小数部分)*2=1.6    取整数部分  1

                         0.6*2=1.2   取整数部分  1

                         0.2*2=0.4   取整数部分  0

                         0.4*2=0.8   取整数部分  0

                         0.8*2=1.6   取整数部分  1

                         0.6*2=1.2   取整数部分  0

                                  .........      0.9二进制表示为(从上往下): 1100100100100......

               注意:上面的计算过程循环了,也就是说*2永远不可能消灭小数部分,这样算法将无限下去。很显然,小数的二进制表示有时是不可能精确的 。其实道理很简单,十进制系统中能不能准确表示出1/3呢?同样二进制系统也无法准确表示1/10。这也就解释了为什么浮点型减法出现了"减不尽"的精度丢失问题。

    2、 float型在内存中的存储

         众所周知、 Java 的float型在内存中占4个字节。float的32个二进制位结构如下

               

             float内存存储结构  

                     4bytes          31                  30            29----23        22----0         

                            表示       实数符号位      指数符号位        指数位          有效数位

            其中符号位1表示正,0表示负。有效位数位24位,其中一位是实数符号位。

             将一个float型转化为内存存储格式的步骤为:

            (1)先将这个实数的绝对值化为二进制格式,注意实数的整数部分和小数部分的二进制方法在上面已经探讨过了。
    (2)将这个二进制格式实数的小数点左移或右移n位,直到小数点移动到第一个有效数字的右边。
    (3)从小数点右边第一位开始数出二十三位数字放入第22到第0位。
    (4)如果实数是正的,则在第31位放入“0”,否则放入“1”。
    (5)如果n 是左移得到的,说明指数是正的,第30位放入“1”。如果n是右移得到的或n=0,则第30位放入“0”。
    (6)如果n是左移得到的,则将n减去1后化为二进制,并在左边加“0”补足七位,放入第29到第23位。如果n是右移得到的或n=0,则将n化为二进制后在左边加“0”补足七位,再各位求反,再放入第29到第23位。

              举例说明: 11.9的内存存储格式

           (1) 将11.9化为二进制后大约是" 1011. 1110011001100110011001100..."。

           (2) 将小数点左移三位到第一个有效位右侧: "1. 011 11100110011001100110 "。 保证有效位数24位,右侧多余的截取(误差在这里产生了 )。

           (3) 这已经有了二十四位有效数字,将最左边一位“1”去掉,得到“ 011 11100110011001100110 ”共23bit。将它放入float存储结构的第22到第0位。

           (4) 因为11.9是正数,因此在第31位实数符号位放入“0”。

           (5) 由于我们把小数点左移,因此在第30位指数符号位放入“1”。

           (6) 因为我们是把小数点左移3位,因此将3减去1得2,化为二进制,并补足7位得到0000010,放入第29到第23位。

               最后表示11.9为:  0 1 0000010 011 11100110011001100110

               再举一个例子:0.2356的内存存储格式
    (1)将0.2356化为二进制后大约是0.00111100010100000100100000。
    (2)将小数点右移三位得到1.11100010100000100100000。
    (3)从小数点右边数出二十三位有效数字,即11100010100000100100000放
    入第22到第0位。
    (4)由于0.2356是正的,所以在第31位放入“0”。
    (5)由于我们把小数点右移了,所以在第30位放入“0”。
    (6)因为小数点被右移了3位,所以将3化为二进制,在左边补“0”补足七
    位,得到0000011,各位取反,得到1111100,放入第29到第23位。

               最后表示0.2356为:0 0 1111100 11100010100000100100000

              将一个内存存储的float二进制格式转化为十进制的步骤:
    (1)将第22位到第0位的二进制数写出来,在最左边补一位“1”,得到二十四位有效数字。将小数点点在最左边那个“1”的右边。
    (2)取出第29到第23位所表示的值n。当30位是“0”时将n各位求反。当30位是“1”时将n增1。
    (3)将小数点左移n位(当30位是“0”时)或右移n位(当30位是“1”时),得到一个二进制表示的实数。
    (4)将这个二进制实数化为十进制,并根据第31位是“0”还是“1”加上正号或负号即可。

    3、浮点型的减法运算

              浮点加减运算过程比定点运算过程复杂。完成浮点加减运算的操作过程大体分为四步:  
       (1) 0操作数的检查;

                    如果判断两个需要加减的浮点数有一个为0,即可得知运算结果而没有必要再进行有序的一些列操作。

       (2) 比较阶码(指数位)大小并完成对阶;

                    两浮点数进行加减,首先要看两数的 指数位 是否相同,即小数点位置是否对齐。若两数 指数位 相同,表示小数点是对齐的,就可以进行尾数的加减运算。反之,若两数阶码不同,表示小数点位置没有对齐,此时必须使两数的阶码相同,这个过程叫做对阶

                   如何对 阶(假设两浮点数的指数位为 Ex 和 Ey ):

            通过尾数的移位以改变 Ex 或 Ey ,使之相等。 由 于浮点表示的数多是规格化的,尾数左移会引起最高有位的丢失,造成很大误差;而尾数右移虽引起最低有效位的丢失,但造成的误差较小,因此,对阶操作规定 使尾数右移,尾数右移后使阶码作相应增加,其数值保持不变。很显然,一个增加后的阶码与另一个相等,所增加的阶码一定是小阶。因此在对阶时,总是使小阶向大阶看齐 ,即小阶的尾数向右移位 ( 相当于小数点左移 ) ,每右移一位,其阶码加 1 ,直到两数的阶码相等为止,右移的位数等于阶差 △ E
       (3) 尾数(有效数位)进行加或减运算;

                   对阶完毕后就可 有效数位 求和。 不论是加法运算还是减法运算,都按加法进行操作,其方法与定点加减运算完全一样。
    (4) 结果规格化并进行舍入处理。

                   

            浮点数的加减法:具体见http://www.zzslxx.com/wmy/jy/Chap02/2.7.1.htm

    4、 计算12.0f-11.9f

           12.0f 的内存存储格式为:    0 1 0000010 10000000000000000000000     

         11.9f 的内存存储格式为:     0 1 0000010 011 11100110011001100110

     

         可见两数的指数位完全相同,只要对有效数位进行减法即可。

         12.0f-11.9f   结果:         0 1 0000010 00000011001100110011010

          

         将结果还原为十进制为: 0.000 11001100110011010= 0.10000038

  • 相关阅读:
    HTML基础
    JPA+atomikos实现分布式事务
    SpringBoot使用MybatisGenerator操作数据
    Spring data JPA的多数据源实现
    整合Spring Data JPA操作数据及排序分页
    首次git推送到远端
    SpringBoot结合Jpa的post为空和时间问题
    记一次SptingBoot启动报错Error creating bean with name 'requestMappingHandlerAdapter'
    解决IDEA中Cannot resolve table * 的问题
    Sringboot jdbc 操作数据库
  • 原文地址:https://www.cnblogs.com/kalo1111/p/3257429.html
Copyright © 2020-2023  润新知