小數在內存中是如何存儲的?

小數在內存中是以浮點數的形式存儲的。浮點數並非一種數值分類,它和整數、小數、實數等不是一個層面的概念。浮點數是數字(或者說數值)在內存中的一種存儲格式,它和定點數是相對的。python

C語言使用定點數格式來存儲 short、int、long 類型的整數,使用浮點數格式來存儲 float、double 類型的小數。整數和小數在內存中的存儲格式不同。函數

咱們在學習C語言時,一般認爲浮點數和小數是等價的,並無嚴格區分它們的概念,這也並無影響到咱們的學習,緣由就是浮點數和小數是綁定在一塊兒的,只有小數才使用浮點格式來存儲。性能

其實,整數和小數能夠都使用定點格式來存儲,也能夠都使用浮點格式來存儲,但實際狀況倒是,C語言使用定點格式存儲整數,使用浮點格式存儲小數,這是在「數值範圍」和「數值精度」兩項重要指標之間追求平衡的結果,稍後我會給你們帶來深刻的剖析。學習

計算機的設計是一門藝術,不少實用技術都是權衡和妥協的結果。優化

浮點數和定點數中的「點」指的就是小數點!對於整數,能夠認爲小數點後面都是零,小數部分是否存在並不影響整個數字的值,因此乾脆將小數部分省略,只保留整數部分。設計

定點數

所謂定點數,就是指小數點的位置是固定的,不會向前或者向後移動。3d

假設咱們用4個字節(32位)來存儲無符號的定點數,而且約定,前16位表示整數部分,後16位表示小數部分,以下圖所示:code

img

如此一來,小數點就永遠在第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

對各個部分的說明:

  • flt 是要表示的小數。
  • sign 用來表示 flt 的正負號,它的取值只能是 0 或 1:取值爲 0 表示 flt 是正數,取值爲 1 表示 flt 是負數。
  • base 是基數,或者說進制,它的取值大於等於 2(例如,2 表示二進制、10 表示十進制、16 表示十六進制……)。數學中常見的科學計數法是基於十進制的,例如 6.93 × 1013;計算機中的科學計數法能夠基於其它進制,例如 1.001 × 27 就是基於二進制的,它等價於 1001 0000。
  • mantissa 爲尾數,或者說精度,是 base 進制的小數,而且 1 ≤ mantissa < base,這意味着,小數點前面只能有一位數字;
  • exponent 爲指數,是一個整數,可正可負,而且爲了直觀通常採用十進制表示。

下面咱們以 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 大於零,mantissa 中的小數點右移 exponent 位便可還原小數的值;
  • exponent 小於零,mantissa 中的小數點左移 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。

1) 符號的存儲

符號的存儲很容易,就像存儲 short、int 等普通整數同樣,單獨分配出一個位(Bit)來,用 0 表示正數,用 1 表示負數。對於 19.625,這一位的值是 0。

2) 尾數的存儲

當採用二進制形式後,尾數部分的取值範圍爲 1 ≤ mantissa < 2,這意味着:尾數的整數部分必定爲 1,是一個恆定的值,這樣就無需在內存中提現出來,能夠將其直接截掉,只要把小數點後面的二進制數字放入內存中便可。對於 1.0011101,就是把 0011101 放入內存。

咱們不妨將真實的尾數命名爲 mantissa,將內存中存儲的尾數命名爲 mant,那麼它們之間的關係爲:

mantissa = 1.mant

若是 base 採用其它進制,那麼尾數的整數部分就不是固定的,它有多種取值的可能,以十進制爲例,尾數的整數部分多是 1~9 之間的任何一個值,這樣一來尾數的整數部分就不能省略了,必須在內存中體現出來。而將 base 設置爲二進制就能夠節省掉一個位(Bit)的內存,這也算是採用二進制的一點點優點。

3) 指數的存儲

指數是一個整數,而且有正負之分,不但須要存儲它的值,還得能區分出正負號來。

short、int、long 等類型的整數在內存中的存儲採用的是補碼加符號位的形式,數值在寫入內存以前必須先進行轉換,讀取之後還要再轉換一次。可是爲了提升效率,避免繁瑣的轉換,指數的存儲並無採用補碼加符號位的形式,而是設計了一套巧妙的解決方案,稍等我會爲您解開謎團。

爲二進制浮點數分配內存

C語言中經常使用的浮點數類型爲 float 和 double;float 始終佔用 4 個字節,double 始終佔用 8 個字節。

下圖演示了 float 和 double 的存儲格式:

img

浮點數的內存被分紅了三部分,分別用來存儲符號 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。

用代碼驗證 float 的存儲

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 位。

IEEE 754 標準

浮點數的存儲以及加減乘除運算是一個比較複雜的問題,不少小的處理器在硬件指令方面甚至不支持浮點運算,其餘的則須要一個獨立的協處理器來處理這種運算,只有最複雜的處理器纔會在硬件指令集中支持浮點運算。省略浮點運算,能夠將處理器的複雜度減半!若是硬件不支持浮點運算,那麼只能經過軟件來實現,代價就是須要容忍不良的性能。

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 時,再也不做爲「正常」的浮點數對待,而是做爲特殊值處理:

  • 若是此時尾數 mant 的二進制位都爲 0,則表示無窮大:
    • 若是符號 sign 爲 1,則表示負無窮大;
    • 若是符號 sign 爲 0,則表示正無窮大。
  • 若是此時尾數 mant 的二進制位不全爲 0,則表示 NaN(Not a Number),也即這是一個無效的數字,或者該數字未經初始化。

非規格化浮點數

當指數 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。

+0 和 -0 的表示

對於非規格化浮點數,當尾數 mant 的全部二進制位都爲 0 時,整個浮點數的值就爲 0:

  • 若是符號 sign 爲 0,則表示 +0;
  • 若是符號 sign 爲 1,則表示 -0。

IEEE 754 爲何增長非規格化浮點數

咱們以 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 列出了四種不一樣的舍入模式。

1) 舍入到最接近的值

就是將結果舍入爲最接近且能夠表示的值,這是默認的舍入模式。最近舍入模式和咱們平時所見的「四捨五入」很是相似,但有一個細節不一樣。

對於最近舍入模式,IEEE 754 規定,當有兩個最接近的可表示的值時首選「偶數」值;而對於四捨五入模式,當有兩個最接近的可表示的值時要選較大的值。以十進制爲例,就是對.5的舍入上採用偶數的方式,請看下面的例子。

最近舍入模式:Round(0.5) = 0、Round(1.5) = 二、Round(2.5) = 2
四捨五入模式:Round(0.5) = 一、Round(1.5) = 二、Round(2.5) = 3

2) 向 +∞ 方向舍入(向上舍入)

會將結果朝正無窮大的方向舍入。標準庫函數 ceil() 使用的就是這種舍入模式,例如,ceil(1.324) = 2,Ceil(-1.324) = -1。

3) 向 -∞ 方向舍入(向下舍入)

會將結果朝負無窮大的方向舍入。標準庫函數 floor() 使用的就是這種舍入模式,例如,floor(1.324) = 1,floor(-1.324) = -2。

4) 向 0 舍入(直接截斷)

會將結果朝接近 0 的方向舍入,也就是將多餘的位數直接丟掉。C語言中的類型轉換使用的就是這種舍入模式,例如,(int)1.324 = 1,(int) -1.324 = -1。

總結

與定點數相比,浮點數在精度方面損失不小,可是在取值範圍方面增大不少。犧牲精度,換來取值範圍,這就是浮點數的總體思想。

IEEE 754 標準其實還規定了浮點數的加減乘除運算,但不是本文的內容就不加以討論了

相關文章
相關標籤/搜索