引入
首先我們來看一段代碼,你認爲它會輸出什麼呢?
#include<stdlib.h>
int main()
{
int i = 0;
float j = 1.0;
float sum =0;
for(i = 0 ; i < 20000000 ; i ++)
sum += j;
printf("%f\n",sum);
}
解析:邏輯上就是將1.0進行累加2千萬次。我們預計的結果應該是20000000。
但是結果卻如圖:
毫無疑問,這肯定和float數據類型有關,但是至於爲什麼會出現這個問題,我們一起來分析。
float數據類型如何表示
我們一般都瞭解int數據類型是如何表示的:它是由32bit位組成,若是有符號int,最高位是符號位,低31位表示有效範圍。無符號int ,32位都是表示有效範圍(32位操作系統,本章中所有demo或話術都是基於32位操作系統)。
但是對float數據類型的表示,我相信大多數人都不太瞭解。於是今天我們一起來深入學習,瞭解在工作或學習中需要注意的事項。
float數據類型在內存中的格式如圖所示:
第一部分是符號位,用s表示,用來表示符號位。float類型和int類型不一樣,int類型可以通過unsigned修飾來表示有符號或無符號數據。float類型都是有符號的。
第二部分是8bit的指數位用e表示,我們用1 ~ 254映射到-126 ~ 127這254個有正有負的數上,因爲浮點數不僅要表示很大的數,也要表示很小的數,因此,指數位也應該有負值。
第三部分是23位的有效位,用f表示。
用科學表示法,浮點數可以表示如下:
從該表達式中,我們無法表示數據0。因此我們就需要一些約定,來表示一些特殊的數。如圖:
當e=0,並且f=0時,浮點數就表示0。
例子
我們一起以0.5爲例進行分析:
因此,s=0,f=0,e=-1。而e是用1 ~ 254映射-126 ~ 127。因而,e在內存中的應該是126。即0.5在內存中的表示應爲下圖:
精度損失問題
通過浮點數的數據格式我們知道,有效位是23位,因此對於有些數保存在計算機中就會出現精度損失的情況。比如9.1。
9.1=1001.000110011(以0011循環)…轉換爲二進制科學表示法就是9.1=1.001000110011(以0011循環)…x 。因此,s=0,e=3,f=001000110011(以0011循環)。由於f只有23bit,所以就會存在精度缺失。如圖:
至此,9.1保存在內存中的二進制爲010000010 0010 0011001100110011 001,在轉換爲十進制就是9.09999942779541015625。
這就解釋了爲什麼0.3+0.6=0.899999。因爲0.3轉換爲浮點數保存到內存中後,不再是準確的0.3了。0.6也是如此。(有些平臺demo打印出來的是0.900000,那是因爲默認打印位數問題,可通過printf("%1.10f",sum)
控制小數點位數。並且不同的編譯器精度缺失的處理方式不同,我在ubuntu 18的測試環境中,得到的值是大於0.9的)
大數喫小數
上面介紹了浮點數精度損失的問題,我們再來看一下大數喫小數的問題。還是直接來上demo:
實際輸出爲:
從現象上看就是一個很大的浮點數和一個較小的浮點數之和,得到的還是較大的浮點數(大數喫小數)。原因是爲何呢?我們一起探究一下。
其實這就是浮點數加法計算的原理過程分析,核心就是先對齊再計算。
例:0.5 + 0.125
分析:
0.5 = x 1.0 x
0.125 = x 1.0 x
由於0.5和0.125的指數位不相等(-1和-3)需要先對齊(統一爲較大的指數位),即:
0.125 = x 0.01 x (指數位對齊,對應的有效位就要右移)
0.5 + 0.125 = x 1.0 x + x 0.01 x = x 1.01 x
上述就是浮點數求和的過程。我相信,大家應該已經知道大數喫小數的原因了。由於有效位是23位,當較大數是較小數的 (16777216)倍之多時,較小數爲了將指數位對齊,有效位就會右移很多位,導致有效位中整數部分在23bit之後。科學表達式爲: x 0.0 x =0,這也就解釋了開篇的問題。
當浮點數1.0進行累加時,sum爲16777216時,是1.0的倍,之後的累加就出現了大數喫小數。
輸出爲:16777216
思考
通過上面的分享,我們在使用浮點數時,要儘量避免兩個坑:精度缺失和大數喫小數。以下是結合自己的經驗做的總結:
-
工作中儘量減少對浮點型數據的使用
曾經我們的研發總監明確要求我們編碼中不準使用浮點數,主要是因爲我們的業務一般不會用到浮點數。我相信很多公司也要求儘可能少的使用浮點數。但是有些業務需求一定會用到小數,比如:商場中商品的價格表,或者是銀行中賬戶金額。我們可以通過字符串的方式保存這些值(雖然有點浪費內存),就可以避免精度缺失的問題 -
面試中遇到的問題
曾經我在面試的時候,面試官讓我對下面的問題進行編程:例:計算代數式 1++++…+的值
我的解法如下:
#include<stdio.h>
int main()
{
long i,n;
double sum;
scanf("%ld",&n);
sum=0.0;
for(i=1;i<=n;i++)
sum+=1.0/i;
printf("%.12f\n",sum);
return 0;
}
其實這不是正確的解,應該將for循環改爲for(i=n;i>=1;i--)
。這點你get到了嗎?