浮点数表示和需要注意的问题

时间:2023-01-08 12:47:58

// 本文不完全是原创,只是整合了一些知识和观点和自己的测试代码,对与错仅供参考。

  计算空间几何变换和动画中经常使用的浮点数宏符号的含义:

float fSValue = 1.0e-07;//等于1.0e-07,1.00e-07,1e-7,等价于1.0 * 10^-7

   float fValue = -1.0e-7;// -1.0e-7等价于-1.0 * 10 ^-7

    const double EPSILON = 1.00e-07;
    const float   FLT_EPSILON  = 1.192092896e-07F;

    const double  DBL_EPSILON = 2.2204460492503131e-016;



// 计算机底层数字转换:

    //1.10进制转换为n进制,正数除以进制,倒转取余(通过16进制和2进制转换更方便);小数乘以进制数,顺序取整。
    //2.N进制数转换为10进制数,Ax(2^1) + Bx(2 ^0) + Cx(2^-1)数列形式表示,无论正数负数都一样。
    //3.浮点数表示:符号位,指数位(0-127表示负数,(127-256)表示正数),尾数(去掉前面一位)。
    //4.大于1Byte的数字和非数值类型,都要进行小端模式排序存放(对于一个单位结构转换后才能得到正确的值)。
// 整数是直接转换为16进制表示(存储转换为小端模式存储),浮点数需要按照转换公式和特殊公式处理(存储转换为小端模式存储),
// 非数值数据,例如ASCII字符,Unicode会转换为相应的字节代码,或者双字节代码来代替;二进制非数值数据存储, 都可以转换为ASCII,UTF-8表示,
// 会索引到文字的图形贴图或者计算的像素。汉字Ascii为负数,那是解析错误,因为汉字有自己的编码,但是存储和显示出来都是对的。
// 图形数据,颜色像素数据,可以用unsigned int表示,32位,每字节表示一个颜色值,将图形数据提交到显卡,显卡会映射为对应的颜色值。

// 1.浮点数的表示
//小数的处理:  
//0.4=0.5*0+0.25*1+0.125*1+0.0625*0+……  
//实际上这永远算不完!这就是著名的浮点数精度问题。

  //1)浮点数表示为十六进制数
// 公式:将浮点数化为:Sx(2^E)xF 后转换为:(-1)Sx(2^(E + 127))x(F - 1),得到S+(E+127)+(F-1)的二进制数,也就是十六进制数。
// 浮点数表示格式:float:符号位(1) 指数位(8) 尾数位(23); double:也是类似的方式,只是位数不同。
// 对于64位的浮点数,最高的1位是符号位S,接着的11位是指数E,剩下的52位为有效数字F。


// 符号位为0则是正数,1则是负数。
//指数位需有负数不用[-128,127]而用[0,255]表示,负数在[0,127],正数在[128,255]
//尾数的前面都是1.1000,因为没有0.11000,故前面可以省略1

// 特殊情况公式转变(表示正负0或者比2^(-127)更小的小数):当用上面的的公式转换后发现:二进制科学计数法得到真实的 E <= -127,那么转换为十六进制时取0x00; F变为0.xxx形式,首位为0舍去,
// 后面的位数为xxx,符号位还是S.

 
/*转换函数
  string DigitalData::TestFloat(float fData)
  {
  unsigned char *pData = (unsigned char*)&fData;
  char szBuffer[32];
  //再转化为16进制为:47 F1 20 00,最后把它翻过来,就成了:00 20 F1 47。
  unsigned char Tail = *(pData);
  unsigned char Tail2 = *(pData + 1);
  unsigned char Exp = *(pData + 2);
  unsigned char Head = *(pData + 3);
  sprintf_s(szBuffer, "%02x%02x%02x%02x", Head, Exp,Tail2, Tail);
  //printf(szBuffer);
  return string(szBuffer);
  }
  */

/*例子1:123456.0f表示为:11110001001000000.0左移16位后变为:
     1.11100010010000000 * ( 2 ^ 16 )
    符号位:0
    指数:16为正数,加上127为:143二进制为10001111
    尾数:1.11100010010000000去掉首位变为:0.11100010010000000,尾数二进制为:11100010010000000 不足23位在后面补足
    那么我们按照前面讲的格式把它拼起来:
    0 10001111 11100010010000000000000
    01000111 11110001 00100000 00000000
    再转化为16进制为:47 F1 20 00,最后把它翻过来,就成了:00 20 F1 47。

    例子2: 123.456f 正数部分表示为:1111011 x (2 ^0), 小数部分(乘2取整)表示为:0.0111 0100 0001 1000 1 x (2 ^ 0)
    合并起来:1.1110 1101 1101 0000 0110 001 x (2 ^ 6).
    符号位:0(1位), 指数位是127 + 6:10000101(8位),尾数部分(去掉前面的1)是:1110 1101 1101 0000 0110 001(23位)
    转换为16进制是: 42 F6 E8 31 小端模式翻转过来是:31 E8 F6 42 由于乘法精度问题可能不准确

    例子3:纯小数模式:0.0456f,转换为二进制小数是:0000101111010 x (2 ^ -1)
    化为科学计数法格式约为:1.0111101 x (2 ^ -5)。
    符号位:0 ,指数位(127 -5 = 122): 0111 1010,尾数(去掉前面一位约为):0111 010
    二进制为:0011 1101 0011 1010
    转换为16进制:3D 3A ,小端翻转后约为:3A 3D

    准确的数字为:
    123.5f的二进制为:1111011.1 x (2 ^0)等于1.1110111 x (2 ^6)
    符号位为:0 ;指数位为:127 + 6 = 133 为10000101;小数位去掉首位为:1110111
    最终二进制为:0100 0010 1111 0111 00000000 00000000 十六进制为:42F70000 反转存储为:0000F742.
    同理:
    1.5f二进制为:1.1 x (2 ^0)
    符号位:0 ;指数位为:127 + 0 = 127 等于01111111;小数位为:10000000 00000000 0000000
    最终二进制为:00111111 11000000 00000000 00000000 十六进制为:3FC00000 小端存储为:0000C03F

    0.15625的十六进制为:3e200000,存储小端为:0000203e

    0.5为:十六进制为:3F000000
    
    2)十六进制数转换为浮点数
    符号位S需要注意正负,2的指数位E需要减去127得到真正的指数位,小数位F需要前面忽略的1得到真正的小数位(特殊情况例外).
    公式:浮点数十六进制数抽取S/E/F出来后,组装为:(-1)Sx(2^(E - 127))x(F + 1)的小数,也就是表示的浮点数。
    
    特殊情况:
    若E位为0并且F位也为0时表示浮点数0,此时浮点数受S位影响,表现出+0和-0两种0,但数值是相等的。

    比如二进制数0x00000000表示+0,二进制数0x80000000表示-0,其中100000000中的1为符号位。

    特殊情况公式转变(表示正负0或者比2^(-127)更小的小数):当用上面的的公式转换后发现:二进制科学计数法得到真实的 E <= -127,那么转换为十六进制时取E为0x00; F变为0.xxx形式,首位为0舍去, 后面的位数为xxx,符号位还是S.
    若E为0,且F不为0时,那么这时表示的是比1.0 x 2(^-127)更小的数;这个时候S为正负,E为1-127等于-126(因为后面会加上0.使得变小了所以-127变为-126变大一位),
    F取得后面的数xxxx前面加上0.得0.xxxx

    例如:
    将0x00000009拆分,得到第一位符号位s=0,后面8位的指数E=00000000,最后23位的有效数字M=000 0000 0000 0000 0000 1001。
    由于指数E全为0,所以符合上一节的第二种情况。因此,浮点数V就写成:
    V=(-1)^0×0.00000000000000000001001×2^(-126)=1.001×2^(-146)
    显然,V是一个很小的接近于0的正数,所以用十进制小数表示就是0.000000


    若E位为255并且F位为0时表示无穷大的数,此时浮点数受S位影响,例如0x7F800000表示正无穷大,0xFF800000表示负无穷大。当我们使用1个数除以0时,结果将被记作0x7F800000。无穷大的调试表示为:1.#INF,当除以0的时候会发生。INF是infinity 无穷大的意思。

    若E位为255并且F位不为0时表示非数值,也就是说是非法数,例如0x7F800001。非法数的调试表示为:1.#IND, 对负数开平方,对负数取对数,0.0/0.0,0.0*∞, ∞/∞ 等会产生。或者表示为NAN。IND是indeterminate不确定/模糊的意思,NAN是not a number的意思。
   


    函数:
    // 3F000000
    float DigitalData::GetFloatFromString( string &strFloatValue)
    {
    union
    {
    float fValue;
    char szBuffer[4];
    }tempUnion;

    // 十六进制字符拷贝到联合体中,因为浮点数不能左移运算
    for(int i = 0; i < 4; i++)
    {
    int hex = 0;
    string hexStr = strFloatValue.substr(2 *i, 2);
    sscanf(hexStr.c_str(), "%02x", &hex);
    tempUnion.szBuffer[ 3 - i] = hex; // 3-i需要小端模式存放,也就是低字节在低位置
    }

    return tempUnion.fValue;
    }
    
    

    2.浮点数比较精度/运算精度/转换/溢出/通信问题:

    基本所有的浮点数都有精度问题
    二进制小数对于十进制小数来说相当于是离散的因此浮点数也只能前面7位可以对上,故有1.0e-07作为浮点数大小区分(实际为6位但是隐藏了1位所以为7位).
    
    1)比较精度问题:浮点数的比较,精度定位到七位或者六位,或者自定义精度,这样可以得到自己想要的结果。
    所以浮点数比较需要精度定义:
    const double EPSILON = 1.00e-07;
    const float   FLT_EPSILON  = 1.192092896e-07F;

    const double  DBL_EPSILON = 2.2204460492503131e-016;

float fValue = 1e-4;
    if(fValue >0.00001f && fValue < 0.001f)
    {
        int i = 0;
    }


    // 二进制转换为浮点数,指数位需要减去127

    浮点数也不能表达圆周率pi,所以tan(pi/2)不等于正无穷,也不会溢出。下面的C语言代码
    double pi = 3.1415926535897932384626433832795;
    double z = tan(pi/2.0);
    的计算结果为16331239353195370.0,如果用单精度浮点数,则结果为−22877332.0。同样的,sin(pi) != 0。
    由于浮点数计算过程中丢失了精度,浮点运算的性质与数学运算有所不同。浮点加法和乘法不符合结合律和分配律。
    
    // 科学计数法,e其实是Exponent浮点数指数的意思其实底数是2,1.0是尾数,尾数前面的是符号位

    FLT_EPSILON;
    float fSValue = 1.0e-07;//等于1.0e-07,1.00e-07,1e-7,等价于1.0 * 10^-7
    if(fSValue >0.00000001f && fSValue < 0.000001f) // 其实这里的e-7相当于十进制10^-7的小数的意思
    {
        int i = 0; // 将会运行到这里

    }


   float fValue = -1.0e-7;// -1.0e-7等价于-1.0 * 10 ^-7
    if(fValue >-0.000001f && fValue < -0.00000001f)
    {
        int i = 0;// 将会运行到这里
    }


    简单的浮点数比较:
    bool bFloatEqual( float a, float b)
    {
    if(fabs(a - b) < FLT_EPSILON)
    {
    return true;
    }


    return false;
    }

    精确复杂的比较:
    bool AlmostEqual2sComplement(float A, float B, int maxUlps)  
    {  
    // Make sure maxUlps is non-negative and small enough that the  
    // default NAN won't compare as equal to anything.  
    assert(maxUlps > 0 && maxUlps < 4 * 1024 * 1024);  
    int aInt = *(int*)&A;  
    // Make aInt lexicographically ordered as a twos-complement int  
    if (aInt < 0)  
    aInt = 0x80000000 - aInt;  
    // Make bInt lexicographically ordered as a twos-complement int  
    int bInt = *(int*)&B;  
    if (bInt < 0)  
    bInt = 0x80000000 - bInt;  
    int intDiff = abs(aInt - bInt);  
    if (intDiff <= maxUlps)  
    return true;  
    return false;  
    }
    
     
    2)运算精度问题:浮点数的运算也存在有效位,差异大的数相加减会舍掉小的数的精度,高精度尽量用double表示

    float和double的精度是由尾数的位数来决定的。浮点数在内存中是按科学计数法来存储的,其整数部分始终是一个隐含着的“1”,
    由于它是不变的,故不能对精度造成影响。
    float:2^23 = 8388608,一共七位,这意味着最多能有7位有效数字,但绝对能保证的为6位,也即float的精度为6~7位有效数字

    double:2^52 = 4503599627370496,一共16位,同理,double的精度为15~16位。

    Visual C++ 2010上的计算结果为987655296.000000,而实际的真实值为987655308.654322,二进制值为0x4E6B79B2对应987655296,就是987655296.000000。
    可以看出有效值是6位,如果按四舍五入的话可以精确到8位,其中变量b贡献的有效数值只有2位。
    对于这种数量级相差很大的计算,计算结果会保证高位数有效,数量级小的数相对计算结果显的太小了,不能按自身6位的精度保持,而是需要按照计算结果的6位精度保持。

    如果你不想你的浮点数在计算上有什么问题,而且你需要精度准确,正确的解决方案不是搞编译参数,
    而是——你一定要使用精度更高字节数更多的数据类型,比如:double 或是long double。


    3)转换精度问题
    直接float,double和 int long long之间转换,因为内存中的表示差异会得到不一样的值,转换会发生错误

    //int num=9; num是整型变量,设为9
  //float* pFloat=&num;  pFloat表示num的内存地址,但是设为浮点数
    //printf("num的值为:%d\n",num);  显示num的整型值

    4) 溢出问题
    在vc中,可以用float.h中的,用一下函数来判断:

    int _isnan(double x) 判断x是不是无效数据(NAN),是返回1,不是返回0
    int _finite(double x)判断x是不是无穷大(1.#INF),是返回0,不是返回非零值
    int _fpclass(double x)用于检验一个浮点数的类型。


    _fpclass的返回值有:
    _FPCLASS_SNAN     /* signaling NaN */

    //_FPCLASS_QNAN     /* quiet NaN */
    //_FPCLASS_NINF     /* negative infinity */
    //_FPCLASS_NN       /* negative normal */
    //_FPCLASS_ND       /* negative denormal */
    //_FPCLASS_NZ       /* -0 */
    //_FPCLASS_PZ       /* +0 */
    //_FPCLASS_PD       /* positive denormal */
    //_FPCLASS_PN       /* positive normal */
    //_FPCLASS_PINF     /* positive infinity */

/*
    5).通信问题

    浮点型在多个处理器间通信时,传递的数值是它的二进制数,比如说1234.5678这个浮点数的二进制数是0x449A522B
    ,如果使用串口发送的话,就会发现串口里发送的是0x44、0x9A、0x52和0x2B这4个数
    (发送的顺序也可能是逆序,这与约定的字节序有关,与浮点格式无关),
    接收端接收到这4个数字后再组合成为00x449A522B,按照IEEE 754的定义被解析成1234.5678
    (从十六进制解析为十进制时候需要解析符号位,指数位减去127,尾数位加上1,转换为float类型的时候会自动转换),这样就实现浮点数通信了。
    如果两个处理器所使用的浮点数规则不同,则无法传递浮点数。
    想了半天,觉得使用IEEE的浮点数规则来发送是最可靠的,因为任何浮点数都被表示成4个字节,这对发送和接收双方都是个好消息,只要双方都知道要进行浮点数的发送就可以了。而且IEEE格式浮点数的转换是机器内部执行的,我们不再需要任何的转换,不增加运算量,不增加代码量。
    按照这个原则,编写了测试代码如下:
    发送方A:
    float fSend; //A需要发送的浮点数据
     char chSend[4]; //发送缓冲,经过转换后的浮点数据,变成一个字符型数组。
    //以下为转换
    chSend[0] = *((char *)(&fSend));
    chSend[1] = *((char *)(&fSend) + 1);
    chSend[2] = *((char *)(&fSend) + 2);
    chSend[3] = *((char *)(&fSend) + 3);


    此时A就可以将这个数字发送给B了,B接收到的是4个字节表示的一个浮点数,但需要经过如下转换使用:

    float fReceive; //接收到的浮点数
    char chReceive[4];//接收缓冲。B将接收到的4个字节保存在这里。
    //以下为转换
    *((char *)(&fReceive)) = chReceive[0];
    *((char *)(&fReceive) + 1) = chReceive[1];
    *((char *)(&fReceive) + 2) = chReceive[2];
    *((char *)(&fReceive) + 3) = chReceive[3];


    好了,此时的B已经得到了一个浮点数fReceive;


参考文章:

    http://bbs.chinaunix.net/thread-3746530-1-1.html
    http://www.cnblogs.com/zhangziqiu/archive/2011/03/30/computercode.html
    http://bbs.21ic.com/icview-602304-1-1.html

    http://blog.csdn.net/quickbasic411/article/details/5921420

    http://www.ruanyifeng.com/blog/2010/06/ieee_floating-point_representation.html