眾所周知,電腦中的所有資料都是以二進位表示的,浮點數也不例外。然而浮點數的二進位標記法卻不像定點數那麼簡單了。
先澄清一個概念,浮點數並不一定等於小數,定點數也並不一定就是整數。所謂浮點數就是小數點在邏輯上是不固定的,而定點數只能表示小數點固定的數值,具用浮點數或定點數表示某哪一種數要看使用者賦予了這個數的意義是什麼。
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;
////////////不可改動//////////////
}