flaot 數據類型的一些坑(大數吃小數)

引入

首先我們來看一段代碼,你認爲它會輸出什麼呢?

#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數據類型在內存中的格式如圖所示:
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 232^3。因此,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 = (1)0(-1)^0 x 1.0 x 212 ^{-1}
0.125 = (1)0(-1)^0 x 1.0 x 232^{-3}
由於0.5和0.125的指數位不相等(-1和-3)需要先對齊(統一爲較大的指數位),即:
0.125 = (1)0(-1)^0 x 0.01 x 212^{-1} (指數位對齊,對應的有效位就要右移)

0.5 + 0.125 = (1)0(-1)^0 x 1.0 x 212 ^{-1} + (1)0(-1)^0 x 0.01 x 212^{-1} = (1)0(-1)^0 x 1.01 x 212^{-1}

上述就是浮點數求和的過程。我相信,大家應該已經知道大數吃小數的原因了。由於有效位是23位,當較大數是較小數的 2242^{24} (16777216‬)倍之多時,較小數爲了將指數位對齊,有效位就會右移很多位,導致有效位中整數部分在23bit之後。科學表達式爲:(1)0(-1)^0 x 0.0 x 2n2 ^{n}=0,這也就解釋了開篇的問題。

當浮點數1.0進行累加時,sum爲16777216時,是1.0的2242^{24}倍,之後的累加就出現了大數吃小數。
輸出爲:16777216

思考

通過上面的分享,我們在使用浮點數時,要儘量避免兩個坑:精度缺失大數吃小數。以下是結合自己的經驗做的總結:

  1. 工作中儘量減少對浮點型數據的使用
    曾經我們的研發總監明確要求我們編碼中不準使用浮點數,主要是因爲我們的業務一般不會用到浮點數。我相信很多公司也要求儘可能少的使用浮點數。但是有些業務需求一定會用到小數,比如:商場中商品的價格表,或者是銀行中賬戶金額。我們可以通過字符串的方式保存這些值(雖然有點浪費內存),就可以避免精度缺失的問題

  2. 面試中遇到的問題
    曾經我在面試的時候,面試官讓我對下面的問題進行編程:

    例:計算代數式 1+12\frac{1}{2}+13\frac{1}{3}+14\frac{1}{4}+…+1n\frac{1}{n}的值
    我的解法如下:

#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到了嗎?

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章