小數在內存中是以浮點數的形式存儲的。浮點數並非一種數值分類,它和整數、小數、實數等不是一個層面的概念。浮點數是數字(或者說數值)在內存中的一種存儲格式,它和定點數是相對的。
python
C語言使用定點數格式來存儲 short、int、long 類型的整數,使用浮點數格式來存儲 float、double 類型的小數。整數和小數在內存中的存儲格式不同。函數
咱們在學習C語言時,一般認爲浮點數和小數是等價的,並無嚴格區分它們的概念,這也並無影響到咱們的學習,緣由就是浮點數和小數是綁定在一塊兒的,只有小數才使用浮點格式來存儲。性能
其實,整數和小數能夠都使用定點格式來存儲,也能夠都使用浮點格式來存儲,但實際狀況倒是,C語言使用定點格式存儲整數,使用浮點格式存儲小數,這是在「數值範圍」和「數值精度」兩項重要指標之間追求平衡的結果,稍後我會給你們帶來深刻的剖析。學習
計算機的設計是一門藝術,不少實用技術都是權衡和妥協的結果。優化
浮點數和定點數中的「點」指的就是小數點!
對於整數,能夠認爲小數點後面都是零,小數部分是否存在並不影響整個數字的值,因此乾脆將小數部分省略,只保留整數部分。設計
所謂定點數,就是指小數點的位置是固定的,不會向前或者向後移動。3d
假設咱們用4個字節(32位)來存儲無符號的定點數,而且約定,前16位表示整數部分,後16位表示小數部分,以下圖所示:code
如此一來,小數點就永遠在第16位以後,整數部分和小數部分一目瞭然,無論何時,整數部分始終佔用16位(不足16位前置補0),小數部分也始終佔用16位(不足16位後置補0)。例如,在內存中存儲了 10101111 00110001 01011100 11000011,那麼對應的小數就是 10101111 00110001 . 01011100 11000011,很是直觀。blog
小數部分的最後一位多是精確數字,也多是近似數字(由四捨五入、向零舍入等不一樣方式獲得);除此之外,剩餘的31位都是精確數字。從二進制的角度看,這種定點格式的小數,最多有 32 位有效數字,可是能保證的是 31 位;也就是說,總體的精度爲 31~32 位。內存
將內存中的全部位(Bit)都置爲 1,小數的值最大,爲 216 - 2-16,極其接近 216,換算成十進制爲 65 536。將內存中最後一位(第32位)置1,其它位都置0,小數的值最小,爲2-16。
這裏所說的最小值不是 0 值,而是最接近 0 的那個值。
用定點格式來存儲小數,優勢是精度高,由於全部的位都用來存儲有效數字了,缺點是取值範圍過小,不能表示很大或者很小的數字。
在科學計算中,小數的取值範圍很大,最大值和最小值的差距有上百個數量級,使用定點數來存儲將變得很是困難。
例如,電子的質量爲:
0.0000000000000000000000000009 克 = 9 × 10-28 克
太陽的質量爲:
2000000000000000000000000000000000 克 = 2 × 1033 克
若是使用定點數,那麼只能按照=
前面的格式來存儲,這將須要很大的一塊內存,大到須要幾十個字節。
更加科學的方案是按照=
後面的指數形式來存儲,這樣不但節省內存,也很是直觀。這種以指數的形式來存儲小數的解決方案就叫作浮點數。浮點數是對定點數的升級和優化,克服了定點數取值範圍過小的缺點。
C語言標準規定,小數在內存中以科學計數法的形式來存儲,具體形式爲:
flt = (-1)sign × mantissa × baseexponent
對各個部分的說明:
下面咱們以 19.625 爲例來演示如何將小數轉換爲浮點格式。
當 base 取值爲 10 時,19.625 的浮點形式爲:
19.625 = 1.9625 × 101
當 base 取值爲 2 時,將 19.625 轉換成二進制爲 10011.101,用浮點形式來表示爲:
19.625 = 10011.101 = 1.0011101×24
19.625 整數部分的二進制形式爲:
19 = 1×24 + 0×23 + 0×22 + 1×21 + 1×20 = 10011
小數部分的二進制形式爲:
0.625 = 1×2-1 + 0×2-2 + 1×2-3 = 101
將整數部分和小數部分合並在一塊兒:
19.625 = 10011.101
能夠看出,當基數(進制)base 肯定之後,指數 exponent 實際上就成了小數點的移動位數:
換句話說,將小數轉換成浮點格式後,小數點的位置發生了浮動(移動),而且浮動的位數和方向由 exponent 決定,因此咱們將這種表示小數的方式稱爲浮點數。
雖然C語言標準沒有規定 base 使用哪一種進制,可是在實際應用中,各類編譯器都將 base 實現爲二進制,這樣不只貼近計算機硬件(任何數據在計算機底層都以二進制形式表示),還能減小轉換次數。
接下來咱們就討論一下如何將二進制形式的浮點數放入內存中。
原則上講,上面的科學計數法公式中,符號 sign、尾數 mantissa、基數 base 和指數 exponent 都是不肯定因素,都須要在內存中體現出來。可是如今基數 base 已經肯定是二進制了,就不用在內存中體現出來了,這樣只須要在內存中存儲符號 sign、尾數 mantissa、指數 exponent 這三個不肯定的元素就能夠了。
仍然以 19.625 爲例,將它轉換成二進制形式的浮點數格式:
19.625 = 1.0011101×24
此時符號 sign 爲 0,尾數 mantissa 爲 1.0011101,指數 exponent 爲 4。
符號的存儲很容易,就像存儲 short、int 等普通整數同樣,單獨分配出一個位(Bit)來,用 0 表示正數,用 1 表示負數。對於 19.625,這一位的值是 0。
當採用二進制形式後,尾數部分的取值範圍爲 1 ≤ mantissa < 2,這意味着:尾數的整數部分必定爲 1,是一個恆定的值,這樣就無需在內存中提現出來,能夠將其直接截掉,只要把小數點後面的二進制數字放入內存中便可。對於 1.0011101,就是把 0011101 放入內存。
咱們不妨將真實的尾數命名爲 mantissa,將內存中存儲的尾數命名爲 mant,那麼它們之間的關係爲:
mantissa = 1.mant
若是 base 採用其它進制,那麼尾數的整數部分就不是固定的,它有多種取值的可能,以十進制爲例,尾數的整數部分多是 1~9 之間的任何一個值,這樣一來尾數的整數部分就不能省略了,必須在內存中體現出來。而將 base 設置爲二進制就能夠節省掉一個位(Bit)的內存,這也算是採用二進制的一點點優點。
指數是一個整數,而且有正負之分,不但須要存儲它的值,還得能區分出正負號來。
short、int、long 等類型的整數在內存中的存儲採用的是補碼加符號位的形式,數值在寫入內存以前必須先進行轉換,讀取之後還要再轉換一次。可是爲了提升效率,避免繁瑣的轉換,指數的存儲並無採用補碼加符號位的形式,而是設計了一套巧妙的解決方案,稍等我會爲您解開謎團。
C語言中經常使用的浮點數類型爲 float 和 double;float 始終佔用 4 個字節,double 始終佔用 8 個字節。
下圖演示了 float 和 double 的存儲格式:
浮點數的內存被分紅了三部分,分別用來存儲符號 sign、尾數 mantissa 和指數 exponent ,當浮點數的類型肯定後,每一部分的位數就是固定的。
符號 sign 能夠不加修改直接放入內存中,尾數 mantissa 只須要將小數部分放入內存中,最讓人疑惑的是指數 exponent 如何放入內存中,這也是咱們在前面留下的一個謎團,下面咱們以 float 爲例來揭開謎底。
float 的指數部分佔用 8 Bits,能表示從 0~255 的值,取其中間值 127,指數在寫入內存前先加上127,讀取時再減去127,正數負數就顯而易見了。19.625 轉換後的指數爲 4,4+127 = 131,131 換算成二進制爲 1000 0011,這就是 19.626 的指數部分在 float 中的最終存儲形式。
先肯定內存中指數部分的取值範圍,獲得一箇中間值,寫入指數時加上這個中間值,讀取指數時減去這個中間值,這樣符號和值就都能肯定下來了。
中間值的求取有固定的公式。設中間值爲 median,指數部分佔用的內存爲 n 位,那麼中間值爲:
median = 2n-1 - 1
對於 float,中間值爲 28-1 - 1 = 127;對於 double,中間值爲 211-1 -1 = 1023。
咱們不妨將真實的指數命名爲 exponent,將內存中存儲的指數命名爲 exp,那麼它們之間的關係爲:
exponent = exp - median
也能夠寫做:
exp = exponent + median
爲了方便後續文章的編寫,這裏我強調一下命名:
- mantissa 表示真實的尾數,包括整數部分和小數部分;mant 表示內存中存儲的尾數,只有小數部分,省略了整數部分。
- exponent 表示真實的指數,exp 表示內存中存儲的指數,exponent 和 exp 並不相等,exponent 加上中間數 median 纔等於 exp。
19.625 轉換成二進制的指數形式爲:
19.625 = 1.0011101×24
此時符號爲 0;尾數爲 1.0011101,截掉整數部分後爲 0011101,補齊到 23 Bits 後爲 001 1101 0000 0000 0000 0000;指數爲 4,4+127 = 131,131 換算成二進制爲 1000 0011。
綜上所述,float 類型的 19.625 在內存中的值爲:0 - 10000011 - 001 1101 0000 0000 0000 0000。
下面咱們經過代碼來驗證一下:
#include <stdio.h> #include <stdlib.h> //浮點數結構體 typedef struct { unsigned int nMant : 23; //尾數部分 unsigned int nExp : 8; //指數部分 unsigned int nSign : 1; //符號位 } FP_SINGLE; int main() { char strBin[33] = { 0 }; float f = 19.625; FP_SINGLE *p = (FP_SINGLE*)&f; itoa(p->nSign, strBin, 2); printf("sign: %s\n", strBin); itoa(p->nExp, strBin, 2); printf("exp: %s\n", strBin); itoa(p->nMant, strBin, 2); printf("mant: %s\n", strBin); return 0; }
運行結果:
sign: 0
exp: 10000011
mant: 111010000000000000000
mant 的位數不足,在前面補齊兩個 0 便可。
printf() 不能直接輸出二進制形式,這裏咱們藉助 itoa() 函數將十進制數轉換成二進制的字符串,再使用
%s
輸出。itoa() 雖然不是標準函數,可是大部分編譯器都支持。不過 itoa() 在 C99 標準中已經被指定爲不可用函數,在一些嚴格遵循 C99 標準的編譯器下會失效,甚至會引起錯誤,例如在 Xcode(使用 LLVM 編譯器)下就會編譯失敗。若是 itoa() 無效,請使用%X
輸出十六進制形式,十六進制可以很方便地轉換成二進制。
對於十進制小數,整數部分轉換成二進制使用「展除法」(就是不斷除以 2,直到餘數爲 0),一個有限位數的整數必定能轉換成有限位數的二進制。可是小數部分就不必定了,小數部分轉換成二進制使用「乘二取整法」(就是不斷乘以 2,直到小數部分爲 0),一個有限位數的小數並不必定能轉換成有限位數的二進制,只有末位是 5 的小數才有可能轉換成有限位數的二進制,其它的小數都不行。
float 和 double 的尾數部分是有限的,當然不能容納無限的二進制;即便小數可以轉換成有限的二進制,也有可能會超出尾數部分的長度,此時也不能容納。這樣就必須「四捨五入」,將多餘的二進制「處理掉」,只保留有效長度的二進制,這就涉及到了精度的問題。也就是說,浮點數不必定能保存真實的小數,頗有可能保存的是一個近似值。
對於 float,尾數部分有 23 位,再加上一個隱含的整數 1,一共是 24 位。最後一位多是精確數字,也多是近似數字(由四捨五入、向零舍入等不一樣方式獲得);除此之外,剩餘的23位都是精確數字。從二進制的角度看,這種浮點格式的小數,最多有 24 位有效數字,可是能保證的是 23 位;也就是說,總體的精度爲 23~24 位。若是轉換成十進制,224 = 16 777 216,一共8位;也就是說,最多有 8 位有效數字,可是能保證的是 7 位,從而得出總體精度爲 7~8 位。
對於 double,同理可得,二進制形式的精度爲 52~53 位,十進制形式的精度爲 15~16 位。
浮點數的存儲以及加減乘除運算是一個比較複雜的問題,不少小的處理器在硬件指令方面甚至不支持浮點運算,其餘的則須要一個獨立的協處理器來處理這種運算,只有最複雜的處理器纔會在硬件指令集中支持浮點運算。省略浮點運算,能夠將處理器的複雜度減半!若是硬件不支持浮點運算,那麼只能經過軟件來實現,代價就是須要容忍不良的性能。
PC 和智能手機上的處理器就是最複雜的處理器了,它們都能很好地支持浮點運算。
在六七十年代,計算機界對浮點數的處理比較混亂,各家廠商都有本身的一套規則,缺乏統一的業界標準,這給數據交換、計算機協同工做帶來了很大不便。
做爲處理器行業的老大,Intel 早就意識到了這個問題,並打算一統浮點數的世界。Intel 在研發 8087 浮點數協處理器時,聘請到加州大學伯克利分校的 William Kahan 教授(最優秀的數值分析專家之一)以及他的兩個夥伴,來爲 8087 協處理器設計浮點數格式,他們的工做完成地如此出色,設計的浮點數格式具備足夠的合理性和先進性,被 IEEE 組織採用爲浮點數的業界標準,並於 1985 年正式發佈,這就是 IEEE 754 標準,它等同於國際標準 ISO/IEC/IEEE 60559。
IEEE 是 Institute of Electrical and Electronics Engineers 的簡寫,中文意思是「電氣和電子工程師協會」。
IEEE 754 簡直是天才通常的設計,William Kahan 教授也所以得到了 1987 年的圖靈獎。圖靈獎是計算機界的「諾貝爾獎」。
目前,幾乎全部的計算機都支持 IEEE 754 標準,大大改善了科學應用程序的可移植性,C語言編譯器在實現浮點數時也採用了該標準。
不過,IEEE 754 標準的出現晚於C語言標準(最先的 ANSI C 標準於 1983 年發佈),C語言標準並無強制編譯器採用 IEEE 754 格式,只是說要使用科學計數法的形式來表示浮點數,可是編譯器在實現浮點數時,都採用了 IEEE 754 格式,這既符合C語言標準,又符合 IEEE 標準,何樂而不爲。
IEEE 754 標準規定,當指數 exp 的全部位都爲 1 時,再也不做爲「正常」的浮點數對待,而是做爲特殊值處理:
當指數 exp 的全部二進制位都爲 0 時,狀況也比較特殊。
對於「正常」的浮點數,尾數 mant 隱含的整數部分爲 1,而且在讀取浮點數時,內存中的指數 exp 要減去中間值 median 才能還原真實的指數 exponent,也即:
mantissa = 1.mant exponent = exp - median
可是當指數 exp 的全部二進制位都爲 0 時,一切都變了!尾數 mant 隱含的整數部分變成了 0,而且用 1 減去中間值 median 才能還原真實的指數 exponent,也即:
mantissa = 0.mant exponent = 1 - median
對於 float,exponent = 1 - 127 = -126,指數 exponent 的值恆爲 -126;對於 double,exponent = 1 - 1023 = -1022,指數 exponent 的值恆爲 -1022。
當指數 exp 的全部二進制位都是 0 時,咱們將這樣的浮點數稱爲「非規格化浮點數」;當指數 exp 的全部二進制位既不全爲 0 也不全爲 1 時,咱們稱之爲「規格化浮點數」;當指數 exp 的全部二進制位都是 1 時,做爲特殊值對待。 也就是說,到底是規格化浮點數,仍是非規格化浮點數,仍是特殊值,徹底看指數 exp。
對於非規格化浮點數,當尾數 mant 的全部二進制位都爲 0 時,整個浮點數的值就爲 0:
咱們以 float 類型爲例來講明。
對於規格化浮點數,當尾數 mant 的全部位都爲 0、指數 exp 的最低位爲 1 時,浮點數的絕對值最小(符號 sign 的取值不影響絕對值),爲 1.0 × 2-126,也即 2-126。
對於通常的計算,這個值已經很小了,很是接近 0 值了,可是對於科學計算,它或許還不夠小,距離 0 值還不夠近,非規格化浮點數就是來彌補這一缺點的:非規格化浮點數可讓最小值更小,更加接近 0 值。
對於非規格化浮點數,當尾數的最低位爲 1 時,浮點數的絕對值最小,爲 2-23 × 2-126 = 2-149,這個值比 2-126 小了 23 個數量級,更加即接近 0 值。
讓我更加驚訝的是,規格化浮點數可以很平滑地過分到非規格化浮點數,它們之間不存在「斷層」,下表可以讓讀者看得更加直觀。
說明 | float 內存 | exp | exponent | mant | mantissa | 浮點數的值 flt |
---|---|---|---|---|---|---|
0值 最小非規格化數 最大非規格化數 | 0 - 00...00 - 00...00 0 - 00...00 - 00...01 0 - 00...00 - 00...10 0 - 00...00 - 00...11 …… 0 - 00...00 - 11...10 0 - 00...00 - 11...11 | 0 0 0 0 …… 0 0 | -126 -126 -126 -126 …… -126 -126 | 0 2^-23 2^-22 1.1 × 2^-22 …… 0.11...10 0.11...11 | 0 2^-23 2^-22 1.1 × 2^-22 …… 0.11...10 0.11...11 | +0 2^-149 2^-148 1.1 × 2^-148 …… 1.11...10 × 2^-127 1.11...11 × 2^-127 |
最小規格化數 最大規格化數 | 0 - 00...01 - 00...00 0 - 00...01 - 00...01 …… 0 - 00...10 - 00...00 0 - 00...10 - 00...01 …… 0 - 11...10 - 11...10 0 - 11...10 - 11...11 | 1 1 …… 2 2 …… 254 254 | -126 -126 …… -125 -125 127 127 | 0.0 0.00...01 …… 0.0 0.00...01 …… 0.11...10 0.11...11 | 1.0 1.00...01 …… 1.0 1.00...01 …… 1.11...10 1.11...11 | 1.0 × 2^-126 1.00...01 × 2^-126 …… 1.0 × 2^-125 1.00...01 × 2^-125 …… 1.11...10 × 2^127 1.11...11 × 2^127 |
0 - 11...11 - 00...00 | - | - | - | - | +∞ | |
0 - 11...11 - 00...01 …… 0 - 11...11 - 11...11 | - | - | - | - | NaN |
^ 表示次方,例如 2^10 表示 2 的 10 次方。
上表演示了正數時的情形,負數與此相似。請讀者注意觀察最大非規格化數和最小規格化數,它們是連在一塊兒的,是平滑過渡的。
浮點數的尾數部分 mant 所包含的二進制位有限,不可能表示太長的數字,若是尾數部分過長,在放入內存時就必須將多餘的位丟掉,取一個近似值。究竟該如何來取這個近似值,IEEE 754 列出了四種不一樣的舍入模式。
就是將結果舍入爲最接近且能夠表示的值,這是默認的舍入模式。最近舍入模式和咱們平時所見的「四捨五入」很是相似,但有一個細節不一樣。
對於最近舍入模式,IEEE 754 規定,當有兩個最接近的可表示的值時首選「偶數」值;而對於四捨五入模式,當有兩個最接近的可表示的值時要選較大的值。以十進制爲例,就是對.5
的舍入上採用偶數的方式,請看下面的例子。
最近舍入模式:Round(0.5) = 0、Round(1.5) = 二、Round(2.5) = 2
四捨五入模式:Round(0.5) = 一、Round(1.5) = 二、Round(2.5) = 3
會將結果朝正無窮大的方向舍入。標準庫函數 ceil() 使用的就是這種舍入模式,例如,ceil(1.324) = 2,Ceil(-1.324) = -1。
會將結果朝負無窮大的方向舍入。標準庫函數 floor() 使用的就是這種舍入模式,例如,floor(1.324) = 1,floor(-1.324) = -2。
會將結果朝接近 0 的方向舍入,也就是將多餘的位數直接丟掉。C語言中的類型轉換使用的就是這種舍入模式,例如,(int)1.324 = 1,(int) -1.324 = -1。
與定點數相比,浮點數在精度方面損失不小,可是在取值範圍方面增大不少。犧牲精度,換來取值範圍,這就是浮點數的總體思想。
IEEE 754 標準其實還規定了浮點數的加減乘除運算,但不是本文的內容就不加以討論了