析IEEE浮点数表示法!

众所周知,计算机中的所有数据都是以二进制表示的,浮点数也不例外。然而浮点数的二进制表示法却不像定点数那么简单了。

先澄清一个概念,浮点数并不一定等于小数,定点数也并不一定就是整数。所谓浮点数就是小数点在逻辑上是不固定的,而定点数只能表示小数点固定的数值,具用浮点数或定点数表示某哪一种数要看用户赋予了这个数的意义是什么。

C++中的浮点数有6种,分别是:

float:单精度,32位
unsigned float:单精度无符号,32位
double:双精度,64位
unsigned double:双精度无符号,64位
long double:高双精度,80位
unsigned long double:高双精度无符号,80位(嚯,应该是C++中最长的内置类型了吧!)

然而不同的编译器对它们的支持也略有不同,据我所知,很多编译器都没有按照IEEE规定的标准80位支持后两种浮点数的,大多数编译器将它们视为double,或许还有极个别的编译器将它们视为128位?!对于128位的long double我也仅是听说过,没有求证,哪位高人知道这一细节烦劳告知。

下面我仅以float(带符号,单精度,32位)类型的浮点数说明C++中的浮点数是如何在内存中表示的。先讲一下基础知识,纯小数的二进制表示。(纯小数就是没有整数部分的小数,讲给小学没好好学的人)

纯小数要想用二进制表示,必须先进行规格化,即化为 1.xxxxx * ( 2 ^ n ) 的形式(“^”代表乘方,2 ^ n表示2的n次方)。对于一个纯小数D,求n的公式如下:
n = 1 + log2(D);// 纯小数求得的n必为负数
再用 D / ( 2 ^ n ) 就可以得到规格化后的小数了。接下来就是十进制到二进制的转化问题,为了更好的理解,先来看一下10进制的纯小数是怎么表示的,假设有纯小数D,它小数点后的每一位数字按顺序形成一个集合:
{k1, k2, k3, ... , kn}
那么D又可以这样表示:
D = k1 / (10 ^ 1 ) + k2 / (10 ^ 2 ) + k3 / (10 ^ 3 ) + ... + kn / (10 ^ n )
推广到二进制中,纯小数的表示法即为:
D = b1 / (2 ^ 1 ) + b2 / (2 ^ 2 ) + b3 / (2 ^ 3 ) + ... + bn / (2 ^ n )
现在问题就是怎样求得b1, b2, b3,……,bn。算法描述起来比较复杂,还是用数字来说话吧。声明一下,1 / ( 2 ^ n )这个数比较特殊,我称之为位阶值。
例如0.456,第1位,0.456小于位阶值0.5故为0;第2位,0.456大于位阶值0.25,该位为1,并将0.45减去0.25得0.206进下一位;第3位,0.206大于位阶值0.125,该位为1,并将0.206减去0.125得0.081进下一位;第4位,0.081大于0.0625,为1,并将0.081减去0.0625得0.0185进下一位;第5位0.0185小于0.03125……
最后把计算得到的足够多的1和0按位顺序组合起来,就得到了一个比较精确的用二进制表示的纯小数了,同时精度问题也就由此产生,许多数都是无法在有限的n内完全精确的表示出来的,我们只能利用更大的n值来更精确的表示这个数,这就是为什么在许多领域,程序员都更喜欢用double而不是float。

float的内存结构,我用一个带位域的结构体描述如下:
struct MYFLOAT
{
bool bSign : 1;  // 符号,表示正负,1位
char cExponent : 8;  // 指数,8位
unsigned long ulMantissa : 23; // 尾数,23位
};

符号就不用多说了,1表示负,0表示正
指数是以2为底的,范围是 -128 到 127,实际数据中的指数是原始指数加上127得到的,如果超过了127,则从-128开始计,其行为和X86架构的CPU处理加减法的溢出是一样的。比如:127 + 2 = -127;127 - 2 = 127
尾数都省去了第1位的1,所以在还原时要先在第一位加上1。它可能包含整数和纯小数两部分,也可能只包含其中一部分,视数字大小而定。对于带有整数部分的浮点数,其整数的表示法有两种,当整数大于十进制的16777215时使用的是科学计数法,如果小于或等于则直接采用一般的二进制表示法。科学计数法和小数的表示法是一样的。
小数部分则是直接使用科学计数法,但形式不是X * ( 10 ^ n ),而是X * ( 2 ^ n )。拆开来看。

0000000000000000000000000000000
符号位指数位尾数位


下面是一个分析float类型内存数据的程序,经测试是完好的,如果发现有问题,请提出来,我好改进。

#include <iostream>
#include <iomanip>
using namespace std;

int _tmain( int argc, _TCHAR* argv[] )
{
// 有符号的32位float类型浮点数变量的定义及数据初始化,其值可任意修改
float fDigital = 0.0f;
// 临时变量,用于存储浮点数的内存数据
unsigned long nMem;
// 将内存按位复制到临时变中,以便取用。
nMem = *(unsigned long*)&fDigital;

// 以小数点后保留8个小数点的精度输出原始浮点数
cout << setprecision( 8 );
cout << "浮点数:" <<  fDigital << endl;
cout << "-----------------------" << endl;

// 判断是否为0,全部位都为0的浮点数据表示0,无分析意义
if ( nMem != 0 )
{
// 打印出其符号。
// 最高1位为符号位。用bool来表示,true表示负数,false表示正数。
// 和最高位为1的数据0x80000000进行按位与,可得到其符号。
bool bNegative = ( ( nMem & 0x80000000L ) != 0 );
// 如果是负数,则输入负号,否则输出空格。
cout << "符号:" << ( bNegative ? '-' : '+' ) << endl;

// 打印出其指数。
// 第30 - 23位是指数位,是有正负的8位整数数据,用char来表示。
// 将内存右移一位,再左移24位,再硬性截断为8位即可得到指数原始数据。
char cExponent = (char)( ( nMem << 1 ) >> 24 );
// IEEE浮点数表示法规定,原指数加127为内存中的指数。
// 将原始指数数据减127得到其真实指数(CPU会自动处理上下界溢出)。
cExponent -= 127;
// 以10进制带符号整数方式输出指数数据
cout << "指数:" << (int)cExponent << endl;

// 打印出其尾数。
// 第22 - 0位是尾数位,由于省去了头一位1,因此应该是24位无符号数据。
// 用无符号长整型来表示。
// 和最低22位都是1的数据0x7FFFFF进行按位与,可得到尾数原始数据
unsigned long ulMantissa = ( nMem & 0x7FFFFFL );
// 和第23位是1的数据0x800000进行按位或,可补齐省去的最高位1
ulMantissa |= 0x800000L;
// 以16进制整数方式输出尾数数据
cout << "尾数:0x" << setbase( 16 ) << setfill( '0' ) << setw( 8 );
cout << setiosflags( ios_base::uppercase ) <<  ulMantissa << endl;

// 完全基于整型算法来实现二进制小数到整数的转换极其复杂,
// 这里借用double简单实现,仅说明其理论。详细算法见说明。
// 计算出浮点数的整数部分,用双精度型表示
double dInteger = 0;
// 指数大于或等于0就代表尾数中存在整数部分
if ( cExponent >= 0 )
{
// 如果指数大于23,则说明整数部分以科学计数法表示
if ( cExponent > 23 )
{
// dCurBit用来计算和存储循环中对于每一位的位阶数
double dCurBit = 1.0;
// 循环所有位,计算科学计数法中的小数的十进制形式
for ( int nBitIdx = 0; nBitIdx < 24; nBitIdx++ )
{
// 将整数部分左移当前位加9,将当前位置于最高位;
// 再右移31位可得当前位,再用当前位阶乘以该位,
// 即得累加数。累加计入二进制小数。
dInteger += dCurBit * ( ( ulMantissa <<
( 8 + nBitIdx ) ) >> 31 );
// 由当前位阶除以2得到下次位阶。
dCurBit /= 2;
}
// 将二进制表示的科学计数法转换为10进制
dInteger *= pow( 2.0, (double)cExponent );
}
else
{
// 将尾数右移小数部分的长度,即可得到整数部分
int nRightShift = 0;
// 如果指数小于23,则说明存在小数部分,需要右移
if ( cExponent < 23 )
{
// 尾数的原始长度减去指数即为小数部分长度
nRightShift = 23 - cExponent;
}
// 将尾数数据右移小数部分的长度,可得到整数部分。
dInteger = (double)( ulMantissa >> nRightShift );
}
}
// 以10进制无符号整数方式输出整数部分
cout << "整数部分:" << setbase( 10 ) << setprecision( 8 );
cout << dInteger << endl;

// 计算出浮点数的小数部分,用无符号长整型表示
double dDecimal = 0;
// 如果指数小于23,则尾数中含小数部分
if ( cExponent < 23 )
{
// 指数与23的差就是小数部分的长度,详见整数部分的处理。
// 将所有位均为1的32位整数右移32减小数部分长度,即指数加9,
// 可得到用于取得小数部分的mask数。
unsigned long ulDecimalMask = 0xFFFFFFFF;
// 如果指数大于或等于0就代表尾数中存在整数部分
if ( cExponent >= 0 )
{
ulDecimalMask >>= ( 9 + cExponent );
}
else
{
// 如果是纯小数,则必须恢复规格化时删掉的整数1
dDecimal = 1.0;
}
// 将尾数和mask数进行按位与,可得到小数部分
// 二进制小数用无符号长整型表示
unsigned long ulDecimal = ulMantissa & ulDecimalMask;
// dCurBit用来计算和存储循环中对于每一位的位阶数
double dCurBit = 0.5;
// 循环所有位,计算科学计数法中的小数的十进制形式
for ( int nBitIdx = 1; nBitIdx < 24; nBitIdx++ )
{
// 将二进制小数左移当前位加9,将当前位置于最高位;
// 再右移31位可得当前位。
// 用dCurBit乘以当前位,即得累加数。累加计入dDecimal。
dDecimal += dCurBit * ( ( ulDecimal <<
( 8 + nBitIdx ) ) >> 31 );
// 由当前位阶除以2得到下次位阶。
dCurBit /= 2;
}
// 将二进制表示的科学计数法转换为10进制
dDecimal *= pow( 2.0, (double)cExponent );
}
// 以10进制无符号整数方式输出小数部分
cout << "小数部分:" << setbase( 10 ) << setprecision( 8 );
cout << dDecimal << endl;
}
else
{
cout << "浮点数为0,无分析意义" << endl;
}

cout << "-----------------------" << endl;
cout << "分析完毕,程序退出。" << endl << endl;

 

////////////不可改动//////////////
system( "pause" );
//_CrtDumpMemoryLeaks();
return 0;
////////////不可改动//////////////

}

阅读更多
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页