
目錄
這篇文章主要探討了浮點數在計算機中的表示、存儲和精度問題。通過詳細的解釋和示例,您將了解浮點數誤差的根源。文章內容較多,大約3700余字,閱讀時間約為10分鐘,建議先收藏,待有空時再細細品讀。
0.1 + 0.2 為什么不等于 0.3 ?
當被問及浮點數為何存在誤差時,你將如何回答?
沒看完這篇文章之前你可能會回答:"哼,反正我就知道有誤差..."
閱讀完這篇文章后,你將能夠更準確地回答這類問題,讓我們開始這段學習之旅吧!
浮點型在內存中的存儲不是像整形那樣直接存儲的,而是用一種二進制的科學計數法來表示的,具體的數學表達式為
其中,e = E - 127
在計算機科學領域,IEEE 754 是一種標準,用于定義浮點數的表示方法,浮點型數據的存儲格式如下

請務必記住,尾數存儲用原碼,階碼存儲用移碼
e=E-127。注意本文后續使用的e表示科學計數法中的指數部分,E表示存儲格式中的階碼,默認的對象都指單精度的浮點數。
接下來我選擇了一個戀愛腦的數字,將1314.520轉換到32位單精度IEEE 754二進制浮點表示標準。
將整數部分反復除以2,并記錄每次的余數,直到商為0為止。
division?=?quotient?+?remainder;
1314?÷?2?=?657?+?0;
657??÷?2?=?328?+?1;
328??÷?2?=?164?+?0;
164??÷?2?=?82??+?0;
82???÷?2?=?41??+?0;
41???÷?2?=?20??+?1;
20???÷?2?=?10??+?0;
10???÷?2?=?5???+?0;
5????÷?2?=?2???+?1;
2????÷?2?=?1???+?0;
1????÷?2?=?0???+?1;
從上面構造的列表的底部開始取所有余數,即為整數部分的二進制表示。131410=101 0010 00102
將小數部分不斷乘以2,并記錄每次的整數部分,直到小數部分為0或達到所需的精度為止
#) multiplying = integer + fractional part;
1) 0.52 × 2 = 1 + 0.04;
2) 0.04 × 2 = 0 + 0.08;
3) 0.08 × 2 = 0 + 0.16;
4) 0.16 × 2 = 0 + 0.32;
5) 0.32 × 2 = 0 + 0.64;
6) 0.64 × 2 = 1 + 0.28;
7) 0.28 × 2 = 0 + 0.56;
8) 0.56 × 2 = 1 + 0.12;
9) 0.12 × 2 = 0 + 0.24;
10) 0.24 × 2 = 0 + 0.48;
11) 0.48 × 2 = 0 + 0.96;
12) 0.96 × 2 = 1 + 0.92;
13) 0.92 × 2 = 1 + 0.84;
14) 0.84 × 2 = 1 + 0.68;
15) 0.68 × 2 = 1 + 0.36;
16) 0.36 × 2 = 0 + 0.72;
17) 0.72 × 2 = 1 + 0.44;
18) 0.44 × 2 = 0 + 0.88;
19) 0.88 × 2 = 1 + 0.76;
20) 0.76 × 2 = 1 + 0.52;
21) 0.52 × 2 = 1 + 0.04;
22) 0.04 × 2 = 0 + 0.08;
23) 0.08 × 2 = 0 + 0.16;
24) 0.16 × 2 = 0 + 0.32;
雖然我們沒有得到任何等于0的小數部分,但是我們有足夠的迭代(超過尾數限制)。
從頂部開始依次取乘法運算的所有整數部分,即為小數部分的二進制:0.5210=0.1000 0101 0001 1110 1011 10002
前面得出了整數以及小數部分的二進制表示,合并以后即:
1314.5210= 101 0010 0010.1000 0101 0001 1110 1011 10002
將小數點向左移動 10 位,使其左邊只剩下一位非零的數字
1314.5210= 101 0010 0010.1000 0101 0001 1110 1011 10002= 101 0010 0010.1000 0101 0001 1110 1011 10002 ×2 0= 1.0100 1000 1010 0001 0100 0111 1010 1110 002 ×2 10
再回顧一下浮點數的數學表達式 V = (-1) s × M × 2 e 由此可知
s = 0
M = 1.0100 1000 1010 0001 0100 0111 1010 1110 00
e = 10
根據規范化得知指數 e = 10,又根據公式 e = E - 127 可得知道 E=137,所以八位階碼的二進制表示如下所示:
E = 13710 = 1000 10012
由第三步規范化得出的尾數M有34位,但是存儲格式中尾數只有23位,下面劃線的是多出的部分,所以需要對尾數按照一定的方式進行四舍五入。
M = 1. 0100 1000 1010 0001 0100 011 1 1010 1110 00
一共有四種舍入方式,
向偶數舍入,簡單理解就要讓尾數的最后一位為0,讓其保持偶數,能夠被2整除。當尾數的最低位為0時,已經是屬于偶數了,無需處理。當尾數最低位為1時,需要加1,使其保持偶數。
因為本例計算出尾數的最后一位為1,按照就近舍入(向偶舍入)原則需要加1使其保持偶數。
所以經過調整后的M為
M = 0100 1000 1010 0001 0100 011 + 1
M = 0100 1000 1010 0001 0100 100
根據前面的步驟可以得知
s = 0
E = 1000 1001 2
M = 0100 1000 1010 0001 0100 100 2
1324.5210 = 0-1000 1001-0100 1000 1010 0001 0100 1002
我們去一個轉換網站上驗證一下轉換結果,網站鏈接放在文章末尾了。

可以看到,跟我們轉換的結果是相同的,說明網站轉換也是選擇向偶數舍入的。
| 類型 | 符號位 | 指數長度(Bit) | 尾數長度(Bit) |
|---|---|---|---|
| float | 1 | 8 | 23 |
| double | 1 | 11 | 52 |
浮點數的精度是由尾數的位數來決定的。
對于float型浮點數,尾數部分23位,換算成十進制就是 2^23=8388608,所以十進制精度只有6 ~ 7位;
這里的數字6和7可能會引起疑問,如何理解它們呢?
由于浮點數尾數的舍入問題,最后一位可能存在舍入誤差,因此不完全準確。因此,可以準確表示的是后六位,而第七位則可能含有誤差。
對于double型浮點數,尾數部分52位,換算成十進制就是 2^52 = 4503599627370496,所以十進制精度只有15 ~ 16位
| 類型 | 有效位 | 字節數 |
|---|---|---|
| float | 6 - 7 | 4 |
| double | 15 - 16 | 8 |
| 類型 | 最小值 | 最大值 |
|---|---|---|
| float | 1.175494351 E - 38 | 3.402823466 E + 38 |
| double | 2.2250738585072014 E - 308 | 1.7976931348623158 E + 308 |
浮點數的比較通常用兩數之差的絕對值小于一個自定義的數值時,代表兩者相等,如下所示:
/**
?*Author:(公眾號:typedef)
?*/
#define?FLOAT_EPSILON?(0.000001)?//Define?your?own?tolerance
#define?FloatIsEqual(a,?b)?((fabs((a)-(b)))<(FLOAT_EPSILON))
另外一種方法是將浮點數同時放大一個倍數,然后轉成整數之間的比較,比如同時放大10000倍等。
首先階碼E是用移碼表示的,那么問題來了,什么叫移碼?移碼怎么計算?移碼的含義是?浮點數為什么要用移碼表示?
在解答這些知識點時,我們需要下面兩點需要達成一致
移碼是補碼表示中最高符號位取反的結果。舉個例子,上面計算1314.52時,指數是為10的。
+1010 = 0000 10102(真值)
原碼:0000 1010
反碼:0000 1010
補碼:0000 1010
移碼:1000 1010
所以10對應標準的移碼 1000 1010 。
注意浮點數中移碼的計算是非標準的,僅偏移2n-1-1=127。所以移碼的計算公式如下所示,其中n為階碼的位數:
E = e + 2 n-1 - 1
E = e + 127
所以10對應的移碼為137。
它通過將數值加上一個固定的偏移量,使得原本可能是負數的數值變為非負數,從而簡化了計算機中有符號數的表示和比較操作。使得計算機能夠直接使用整數運算來比較浮點數的大小。
浮點數指數部分的實際取值范圍是 [-2(e-1)+2, 2(e-1)-1],其中 e 為指數所占位數。32位浮點數,指數占8位,實際取值范圍是 [-126, 127]。
-127用作表示0,128 用作表示無窮大和 NaN。NaN 是 "Not a Number" 的縮寫,中文意思是“非數字”,通常用于表示一個未定義或不可表示的值。
換言之,8位階碼的表示范圍是[0, 255],其中0和255用于表示特殊值。因此,根據公式推導,指數e的實際取值范圍是[-126, 127]。
| 形式 | 指數(e) | 階碼(E) | 小數部分 |
|---|---|---|---|
| 零 | -127 | 0 | 0 |
| 無窮 | 128 | 2e-1 = 255 | 0 |
| NaN(非數) | 128 | 2e-1 = 255 | 非0 |
此時再來回答文中引言提出的問題, 0.1 + 0.2 為什么不等于 0.3 ?
/**
?*?Author:(公眾號:typedef)
?*/
#include?
int?main()?{
??double?a?=?0.1?+?0.2;
??printf("%.17f",?a);
}
輸出為0.30000000000000004,由于在尾數舍入時會帶來一定的誤差,所以并不完全相等。
當在被問及浮點數為何存在誤差時,你將如何回答?歡迎文章留言說出你的看法。
如果不從技術的角度回答這個問題,可以這樣回答:整數是離散的,有限的并能夠被計算機表示的,小數部分是連續的,包含無窮多的數,數量之多是無法被計算機存儲的,只能存儲計算機能夠表示的最接近這個數值的小數部分,所以可能會不相等。
本篇文章深入分析了浮點數的存儲格式到轉換流程,再到指數e以及階碼E的探索,大家應該對浮點數有了更全面的理解。
碼字不易,如果您覺得有收獲,歡迎點贊、轉發,加關注!
END
點贊、轉發加關注,一鍵三連,好運年年
關注公眾號后臺回復數字688或668可獲取嵌入式相關資料
往期推薦
C語言編程新手:如何判斷結構體(struct)相等?

避免內存陷阱:掌握memcpy和memmove的正確用法

揭秘難以復現Bug的解決之道:堆棧分析實戰

加個變量,程序崩了

