C語言之浮點數(shù)的表示與儲存方式
1. 二進(jìn)制小數(shù)
1.1 十進(jìn)制小數(shù)的表示方法
理解浮點數(shù)的第一步是考慮含有小數(shù)值的十進(jìn)制數(shù)字
先來看一下十進(jìn)制數(shù)字的表示法:
其中每個十進(jìn)制數(shù) 的取值范圍是0~9。這個表達(dá)描述的數(shù)值
的定義如下:
數(shù)字權(quán)的定義與十進(jìn)制小數(shù)點符號( ‘.’ ) 相關(guān),這意味著小數(shù)點左邊的數(shù)字的權(quán)是10的正冪,得到整數(shù)值,而小數(shù)點右邊的數(shù)字的權(quán)是10的負(fù)冪,得到小數(shù)值。例如十進(jìn)制數(shù)12. 34表示數(shù)字
1.2 二進(jìn)制小數(shù)的表示方法
類比十進(jìn)制數(shù),二進(jìn)制表示小數(shù)可以用如下表示法表示,
以圖片表示為:
其中 的取值范圍是0和1。這個表達(dá)描述的數(shù)值
的定義如下:
符號 ‘ . ’ 現(xiàn)在變?yōu)榱硕M(jìn)制的點,點左邊的位的權(quán)是2的正冪,點右邊的位的權(quán)是2的負(fù)冪。例如,二進(jìn)制數(shù)101.11表示數(shù)字
2. IEEE浮點表示
2.1 IEEE浮點標(biāo)準(zhǔn)
上一節(jié)提到的定點表示法并不能很有效地表示非常大的數(shù)字。IEEE(電氣和電子工程師協(xié)會)浮點標(biāo)準(zhǔn)用如下的形式來表示一個數(shù):
在這個式子中設(shè)計三個變量,s, M以及E
符號 (sign) : s決定這數(shù)是負(fù)數(shù) (s = 1) 還是正數(shù) (s = 0)尾數(shù) (ignificand) : M是一個二進(jìn)制小數(shù)階碼 (exponent): E的作用是對浮點數(shù)加權(quán),這個權(quán)重是2的E次冪(可能是負(fù)數(shù))
例如:
5.0 化為二進(jìn)制小數(shù),由于 5.0 為正數(shù),因此 s = 0,M = 1.01,E = 2
這種標(biāo)準(zhǔn)將浮點數(shù)封裝成一種似乎很難理解的形式來存儲,但其實是相當(dāng)優(yōu)雅的。
2.2 單精度和雙精度浮點數(shù)的封裝形式
在C語言中,浮點數(shù)分為單精度浮點數(shù) float 和雙精度浮點數(shù) double,其中float在內(nèi)存中占4個字節(jié),32個比特位;double在內(nèi)存中占8個字節(jié),64個比特位。
不管是 float 還是 double ,它們都被分為三個字段,分別用來表示符號位s,階碼字段exp和編碼尾數(shù)frac。這三個字段與上述的符號s,尾數(shù)M,階碼E一一對應(yīng),但并非將s,M,E直接存入內(nèi)存,而是根據(jù)浮點數(shù)的不同數(shù)值類型按照不同的規(guī)則進(jìn)行編碼。
2.3 浮點數(shù)的數(shù)值分類
根據(jù)階碼的不同,浮點數(shù)的數(shù)值可以分為三類:
- 規(guī)格化的值 (Normalized Values)
- 非規(guī)格化的值 (Denormalized Values)
- 特殊值 (Special Values)
exp的值決定了這個數(shù)屬于上面類型中的哪一種,以float類型為例:
2.3.1 規(guī)格化的值 (Normalized Values)
當(dāng)階碼域不全為0并且不全為1時,表示該數(shù)值為規(guī)格化的值
當(dāng)階碼字段不全為0時,表示該數(shù)值為非規(guī)格化的值。這是一種最普遍的情況,大多數(shù)浮點數(shù)都屬于這類。比如上一小節(jié)的5.0
對于規(guī)格化的值,在得到s, E, M之后還需要進(jìn)行一些處理才能放進(jìn)內(nèi)存:
規(guī)則1:
- 前面已經(jīng)提到,E是階碼并且可以表示負(fù)值,存放在exp字段,但是E在標(biāo)準(zhǔn)中為無符號數(shù),這說明它不能表示負(fù)數(shù)且可表示的范圍為0~255。那么對于E為負(fù)值的情況如何處理呢?
- 這里引入偏置 (Bias) 的概念。
- IEEE 754規(guī)定,存入內(nèi)存時E的真實值必須再加上一個中間數(shù),對于8位的E,這個中間數(shù)是127;對于11位的E,這個中間數(shù)是1023。這里的127和1023就是Bias。也就是說,階碼的值是 E= exp - Bias
- 例如:2^10的E是10,所以保存成32位浮點數(shù)時,必須保存成10+127=137,即10001001。此時內(nèi)存中的exp中存的是10001001。
規(guī)則2
- 同樣的,M存放在小數(shù)字段 f 中,我們知道,當(dāng)一個小數(shù)化為二進(jìn)制小數(shù)后所得到的M總是一個介于1~2之間的值,形式為1. xxxxx
- 因此在存數(shù)據(jù)時,考慮省略小數(shù)點前面的1(取數(shù)據(jù)的時候可以直接在前面添上),可以節(jié)省一位的存儲空間,能讓小數(shù)點后的數(shù)據(jù)多保存一位,提高數(shù)據(jù)的精度。
- 例如,M=1.01101,存到 f 中去的數(shù)據(jù)為01101,M = 1 + f
到這里我們可以解決 5.0 這樣一個規(guī)格化的值的存放問題了:
我們知道,對于5.0來說,s = 0,M = 1.01,E = 2,
根據(jù)以上規(guī)則,在內(nèi)存中,符號位字段s = 0,階碼字段exp = E + 127 = 129 = 10000001,編碼尾數(shù)f = 01000000000000000000000(不夠23位要在后面補(bǔ)0)
結(jié)合來看:
0 10000001 01000000000000000000000,化為十六進(jìn)制為40 a0 00 00
在小端機(jī)器上的結(jié)果為:
2.3.2 非規(guī)格化的值 (Denormalized Values)
當(dāng)階碼域為全0時, 所表示的數(shù)是非規(guī)格化的值
在這種情況下,規(guī)則又有所不同:
規(guī)則1
- 當(dāng)exp為全0時,此時的E = 1- Bias,也就是說1-127(或者1-1023)即為真實值
- 補(bǔ)充:使階碼值為 1-Bias 仍而不是簡單的 -Bias 似乎是違反直覺的。在后面我們會知道,這種方式提供了一種從非規(guī)格化值平滑轉(zhuǎn)換到規(guī)格化值的方法
規(guī)則2
- 有效數(shù)字M不再加上第一位的1,而是還原為0.xxxxxx的小數(shù),即M = f ,也就是小數(shù)字段的值, 不包含隱含的開頭的1
- 實際上,如M = 1.01101,在exp = 00000000時,存進(jìn)去的是101101而不是01101
非規(guī)格化數(shù)有兩個用途:
- 首先,它們提供了一種表示數(shù)值0 的方法,因為使用規(guī)格化數(shù),我們必須總是M>= 1,因此我們就不能表示0.0
- 其次,非規(guī)格化數(shù)的另外一個功能是表示那些非常接近于 0.0 的數(shù)。它們提供了一種屬性,稱為逐漸溢出(gradualunder flow) ,其中,可能的數(shù)值分布均勻地接近于0.0
2.3.3 特殊值 (Special Values)
當(dāng)階碼域為全1時, 所表示的數(shù)是特殊值
特殊值可分為兩種:
1. 當(dāng)小數(shù)域全為0 時,得到的值表示無窮,當(dāng) s = 0 時,是+∞, 或者當(dāng) s = 1時,是 -∞
2.當(dāng)小數(shù)域為非零時, 結(jié)果值被稱為 “NaN”(Not a Number)。一些運(yùn)算的結(jié)果不能是實數(shù)或無窮, 就會返回這樣的NaN值
3. 數(shù)字示例
為了更加直觀地理解,我們用一個8位浮點數(shù)的例子,假定符號位 s 的長度為1,階碼字段的長度為4,小數(shù)字段的長度為3:
對于非規(guī)格數(shù):
對于規(guī)格數(shù)
可以看到,從最大非規(guī)格數(shù)0 0000 111到最小規(guī)格數(shù)0 0001 000這樣的過渡是很自然的,體現(xiàn)出了上述IEEE標(biāo)準(zhǔn)的邏輯性與和諧的美感
4. 舍入
因為表示方法限制了浮點數(shù)的范圍和精度, 所以浮點運(yùn)算只能近似地表示實數(shù)運(yùn)算
我們企圖找到一個與值 最相近的值匹配值
來作為儲存的值
例如:
如果由于表示方法的限制,1.5這樣一個值無法完全放在內(nèi)存中,需要舍掉小數(shù)點后的值,那么舍入結(jié)果是1還是2呢?
IEEE定義了四種舍入方式:
- 向偶數(shù)舍入
- 向零舍入
- 向上舍入
- 向下舍入
其中,向偶數(shù)舍入(round - to - even)又被稱為向最接近的值舍入(round - to - nearest),是默認(rèn)的方式,試圖找到一個最接近的匹配值
向上和向下舍入很好理解,一個介于1~2之間的數(shù)如 1.5 向上舍入是2,向下舍入是1
向零舍入是指在在數(shù)軸上的數(shù)向 0 的方向進(jìn)行舍入,比如 1.50 向零舍入會找到 1 和 2 之間更靠近0 的數(shù) 1 ,-1.50 向零舍入會找到 -1 和 -2 之間更靠近 0 的數(shù) -1
向偶數(shù)舍入,指當(dāng)一個數(shù)是兩個可能結(jié)果的中間數(shù)時,它將數(shù)字向上或者向下舍入,使得結(jié)果的最低有效數(shù)字是偶數(shù)
比如1.50可以向1舍入,也可以向2舍入,并且正好是1和2的中間值,這時會默認(rèn)向2(偶數(shù))舍入;比如2.50可以向2舍入,也可以向3舍入,并且正好是2和3的中間值,這時同樣會向2(偶數(shù))舍入。
值得注意的是:
向偶數(shù)舍入只針對那些“中間值”。當(dāng)值為1.49或1.51時,依舊舍入為 1 和 2
方式 | 1.40 | 1.60 | 1.50 | 2.50 | -1.50 |
向偶數(shù)舍入 | 1 | 2 | 2 | 2 | -2 |
向零舍入 | 1 | 1 | 1 | 2 | -1 |
向上舍入 | 1 | 1 | 1 | 2 | -2 |
向下舍入 | 2 | 2 | 2 | 3 | -1 |
為什么要使用向偶數(shù)舍入呢?
使用其他三種舍入方法,在一組數(shù)據(jù)中很容易引入平均值的統(tǒng)計偏差 ,當(dāng)1.50 1.60 1.70這樣一組數(shù)據(jù)都使用向上舍入,結(jié)果是2 2 2,平均值會偏大
向偶數(shù)舍入在大多數(shù)現(xiàn)實情況中避免了這種統(tǒng)計偏差。在50%的時間里,它將向上舍入,而在50%的時間里,它將向下舍入
對二進(jìn)制的浮點數(shù)舍入同樣遵循向偶數(shù)舍入的原則,并將 0 視為偶數(shù),1 視為奇數(shù)
例如:
10.11100 ,當(dāng)舍入需要精確到小數(shù)點后兩位時, 后三位100代表 ,正好是中間值,因此向偶數(shù)舍入為11.00
5. 浮點運(yùn)算
考慮下面幾個式子:
- (1) (3.14 + 1e10) - 1e10 = 0.0
- (2) 3.14 + (1e10 - 1e10) = 3.14
- (3) (1e20 * 1e20) * 1e-20 = +∞
- (4) 1e20 * (1e20 * 1e-20) = 1e20
- (5) 1e20 * (1e20 - 1e20) = 0.0
- (6) 1e20*1e20 - 1e20*1e20 = NaN
- (1)(2)中,3.14 + 1e10對結(jié)果進(jìn)行了舍入,值3.14會丟失,因此對于浮點數(shù)的加法不具有結(jié)合性
- (3)(4)中,由于計算結(jié)果可能溢出或舍入,因此浮點數(shù)的乘法也不具有結(jié)合性
- (5)(6)中,在單精度浮點數(shù)時結(jié)果不同,說明浮點數(shù)乘法不具有分配性
6. C語言中的浮點數(shù)
所有的C語言版本提供了兩種不同的浮點數(shù)據(jù)類型: float 和 double
當(dāng)int ,float,double不同數(shù)據(jù)類型之間進(jìn)行強(qiáng)制類型轉(zhuǎn)換時,得到的結(jié)果可能會超出我們的預(yù)期,程序改變數(shù)值和位模式的原則如下( 假設(shè)int是32位的) :
- 從 int 轉(zhuǎn)換成 float,數(shù)字不會溢出,但是可能被舍入
- 從 int 或 float 轉(zhuǎn)換成 double,因為double有更高的精度,所以能夠保留精確的數(shù)值
- 從 double 轉(zhuǎn)換成 float ,因為范圍要小一些,所以值可能溢出成 +∞ 或 -∞。另外,由于精確度較小,它還可能被舍入
- 從 float 或者 double 轉(zhuǎn)換成int,值將會向零舍入。例如,1.999將被轉(zhuǎn)換成1,而-1.999將被轉(zhuǎn)換成 -1。進(jìn)一步來說,值可能會溢出
總結(jié)
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
C++ 使用CRC32檢測內(nèi)存映像完整性的實現(xiàn)步驟
當(dāng)我們使用動態(tài)補(bǔ)丁的時候,那么內(nèi)存中同樣不存在校驗效果,也就無法抵御對方動態(tài)修改機(jī)器碼了,為了防止解密者直接對內(nèi)存打補(bǔ)丁,我們需要在硬盤校驗的基礎(chǔ)上,增加內(nèi)存校驗,防止動態(tài)補(bǔ)丁的運(yùn)用。2021-06-06C++?超詳細(xì)分析多態(tài)的原理與實現(xiàn)
這篇文章主要介紹了C++多態(tài)的原理與實現(xiàn),多態(tài)是一種面向?qū)ο蟮脑O(shè)計思路,本身和C++不是強(qiáng)綁定的,其他語言當(dāng)中一樣有多態(tài),只不過實現(xiàn)的方式可能有所不同。下面來一起了解更多詳細(xì)內(nèi)容吧2022-03-03關(guān)于Qt?C++中connect的幾種寫法代碼示例
這篇文章介紹了Qt中connect函數(shù)的不同編寫方式,包括傳統(tǒng)的槽函數(shù)寫法、使用函數(shù)指針的寫法、Lambda表達(dá)式以及使用QOverload選擇重載信號的寫法,每種寫法都有其特點和適用場景,程序員應(yīng)根據(jù)具體需求選擇最合適的方式,需要的朋友可以參考下2024-11-11C語言編程技巧 關(guān)于const和#define的區(qū)別心得
盡量用const和inline而不用#define 這個條款最好稱為:“盡量用編譯器而不用預(yù)處理”,因為#define經(jīng)常被認(rèn)為好象不是語言本身的一部分。這是問題之一。再看下面的語句:2013-02-02