大多數語言都提供從float到double的轉換,比如C語言,你可以直接通過一個賦值語句來實現把一個float的數字轉成 double。而某些蛋疼的語言里面,對二進制的支持實在是少的可憐,我們還是不得不處理這樣蛋疼的問題。
MQL4 這種語言大家可能沒有這么聽說過,是一種寫金融交易策略的語言。我的一個同事在用這種語言寫策略的時候,遇到了一個問題,要從網絡中接收float的二進制數據,然后進行計算,而這種語言只支持double,沒有float的。於是,我這個救火隊員上馬了。
說句實話,我非常喜歡這樣蛋疼的問題。當然,對二進制,底層非常熟悉的人,這基本上不是問題。而我工作了這樣多年,說句實話,我還真不知道 float 和 double的內部是什么樣的一個結構。於是我查找了很多資料,終於基本上搞懂了浮點數,於是我准備把我所學的寫成博客,方便后面的人查看資料。首先就從我解決的這個問題開始吧。
面對一個問題,首先就要從了解“敵人”開始。我首先要知道 float 和 double 是怎么表示一個數字的。有了這些知識,我想就能有辦法把一個float轉成 double。
幸好有google。我找到了著名的 阮一峰 老師的一篇博客。詳細的可以看老師的博客:浮點數的二進制表示
簡單的說,一個浮點數,不管是 float 和 double 由三部分組成。
1. 符號
2. 小數點的位置
3. 有效數字
這符合我們的常識認識,一個小數就是由這三部分組成的。小數點的位置,有時候可以用“科學計數法”來表示,而有效數字可以簡單的認為是一個整數,根據IEEE 754的標准,在一個 float 4個字節,32個位中,這是三個部分分配如下:

(圖片來自 阮一峰 的博客)
符號位是1位,還有 8 位表示 小數點的位置,后面的表示有效數字。為了證明一下,上面結構的數字是 0.15625,我用PHP寫可一段腳本來驗證了一下:
C:\Users\cykzl>php -a
Interactive mode enabled
<?php
$bin = "00111110001000000000000000000000";
$dec = bindec($bin);
$float = unpack("f", pack("L", $dec));
echo $float[1];
?>
^Z
0.15625
C:\Users\cykzl>
果然沒有錯。那么這個數字是怎么算出來的呢?
程序員的話,當然用偽代碼表示算法比較清晰:
sign = 符號 = value >> 31
e = 指數 = ((value >> 23 ) & 0xFF)
m = 有效數字 = value & 0x007FFFFF
現在指數用 小e 來表示,表示,這還不是最終的指數, 還要做分類討論, 我把最終的值設為 E 。
同樣的有效數字部分也要做處理,我們暫時認為這個部分最終的處理結果為 M
最終一個 float(sign, E, M) 用科學計數法表示出來就是
float(sign, E, M)
IF sign == 1
return - M * pow(2, E)
ELSE
return M * pow(2, E)
現在我們看到,e的范圍是:0 - 255,沒有辦法表示負指數,這在表示一個比較小的數字的時候很有用,其實要表示負數也很簡單,
減去一個中間數127,就可以表示 -127 - 128 的范圍了, E = e - 127, 不過還有意外情況,下面再討論。
m有二十三位,能表示的范圍是 0 – 8388607 換算成10進制的話,基本上就是7位有效數字。所以,一個單精度浮點數能表示
的有效數是7位。怎么得到這個 M(有效數字)呢?
首先,m部分要變成一個小數,這個很簡單,decimals = m / (pow(2, 24)) 這樣,decimals 的范圍就是(用10進制表示)
0 – 0x007FFFFF / 0x00800000 = 0 - 0.99999988079071044921875.
二進制表示就是 0 – 0.11111111111111111111111 ,
這還沒有完,中學里面學的科學計數法都是下面的形式:
2.88 * 10 ^ –2
小數點前面還要一位。這十進制,小數點前一位有10種可能性,可是這二進制只有兩種可能性,這樣的話,能不能
通過一個規則,不顯式表示這一位,這樣可以節約一位,這就是IEEE 754 的精華部分。
根據IEEE 754的規則是這樣的:
e = 0 時 E = 1 – 127 = -126, M = 0 + decimals = decimals , 我們簡單的記為 0.m
e > 0 時 E = e – 127, M = 1 + decimals , 我們簡單的記為 1.m
至於e = 0 時,E 為什么不取為 0 –127 而是 1 – 127, 這是為了實現 一個平穩的過渡。簡單的說,就是 e = 0 時最大的數,
和 e = 1 時最小的數要非常的接近。
e = 0時 最大的M 可以 0.99999988079071044921875 ,而最小的 e = 1 時,最小的 M = 1,這兩個M是連續的(非常接近),必須保證指數是一樣的時候,他們才會銜接的很好,這是IEEE 754 用的一點小技巧。
簡單的說:
IF (e ==0)
E = –126
M = 0.m
return float(sign, E, M)
ELSE
E = e – 127
M = 1.m
return float(sign, E, M)
理論上這樣就能計算了。不過 decimals 這個值的計算是不是有點大動干戈,采用10進制來計算:
比如:m = 10000000000000000000000 (二進制) = 0x00400000 (16進制)
變成小數就是 0x00400000 / 0x00800000 = 0.5
其實,采用二進制小數來計算有些時候更加方便(特別是手工計算的時候)。二進制小數可以直接這樣計算
0.10000000000000000000000 = 1 * 2^-1 = 0.5
0.11000000000000000000000 = 1 * 2^-1 + 1 * 2^-2 = 0.5 + 0.25 = 0.75
好,我們就計算一下上圖中的那個小數吧
sign = 0
e = 124
m = 010000000000000000000000 (二進制)
0.m = 0.010000000000000000000000(二進制) = 0.25 (十進制的小數)
E = 124 – 127 = –3
M = 1 + 0.m = 1.25
float(0, –3, 1.25) = 1.25 * 2^-3 = 1.25 / 8 = 0.15625
好了,這樣就計算出來了。
至於double 規則全部一樣,但是精度不一樣:
sign 還是一位
e 11 位,能表示 0 - 2047, 中間數是 1023
m 52 位, double 表示10進制的精度可以到達15位有效數字
從float 到 double 的轉換:
一個數字,不管是float 還是 double 。肯定都有一樣的 sign E M
但是 sign e m 這三個的表示可能有所不同。
可以發現,sign肯定是相同的。
e 和 m 可能不同。
float 的e 我們記為 ef
double 的e 我們記為 ed
這樣 ef – 127 = ed – 1023
ed = ef – 127 + 1023
m 從二進制上看更加直觀, 他們表示的都是一個二進制的小數,所以,應該完全一致,才能表示出一樣的M
利用上面的算法,我表示一下上圖中的float 到double
0 01111111100 0100000000000000000000000000000000000000000000000000
如果你熟悉位運算,那么答案就呼之欲出了:
基本思路: 設置符號位,設置新的e,設置有效數字
考慮到一些語言沒有 float 也 沒有int64 ,完全就用int 來表示這個過程。
int buffer[2];
int sign = value >> 31;
int M = value & 0x007FFFFF;
int e = ((value >> 23 ) & 0xFF) - 127 + 1023;
//小尾的機器
buffer[1] = ((sign & 1) << 31) | ((e & 0x7FF) << 20) | (M >> 3);
buffer[0] = (M & 0x7) << 29;
//大尾的機器
buffer[0] = ((sign & 1) << 31) | ((e & 0x7FF) << 20) | (M >> 3);
buffer[1] = (M & 0x7) << 29;
最后一步是把 buffer, 拷貝到一個 double的空間里面去,你會驚奇的發現,這個double 和 float表示的數字完全一致。
下面是一個MQL4 的完整實現:
double unpack_float(int &pack[], int pos)
{
int value = unpack_int(pack, pos);
int buffer[2];
double d[1];
int sign = value >> 31;
int M = value & 0x007FFFFF;
int e = ((value >> 23 ) & 0xFF) - 127 + 1023;
buffer[1] = ((sign & 1) << 31) | ((e & 0x7FF) << 20) | (M >> 3);
buffer[0] = (M & 0x7) << 29;
memcpy(pd(d), pi(buffer), 8);
return (d[0]);
}
這門蛋疼的語言,沒有char[], 所以,你會發現用了一個int[]
unpack_int 是unpack出一個int的表示
pd 取出 double 數組的指針(只有數組可以取地址,所以不得不用了一個 double[1], 來表示一個double)
pi 取出 int 數組的指針