首頁 > 軟體

C語言深入分析浮點型資料儲存

2022-08-12 22:01:17

1. 前言

上篇文章,我們對整形是如何儲存的做出了講解,而在本篇文章中,我將講解浮點型是如何儲存的,以及對於浮點型資料之間的比較做出一個探究,相信在閱讀本篇文章後,你會對資料在記憶體中的儲存有一個新的認識。話不多說,我們進入正題。

2. 浮點型在記憶體中的儲存

浮點數包括float(單精度浮點數)、double(雙精度浮點數)、long double型別。

常見的浮點數有:

3.14
1E10//1.0 * (10 ^ 10)

而浮點數的取值範圍的相關資訊也在float.h中定義。

例如:

注:DBL_EPSILON和FLT_EPSILON分別是雙精度浮點數和單精度浮點數的精度。

瞭解基本概念之後,我們再看一個關於浮點型儲存的經典樣例。

3. 例題引入

下列程式輸出的結果是什麼?

int main()
{
	int n = 9;
	float* pFloat = (float*)&n;
	printf("n的值為:%dn", n);//9
	printf("*pFloat的值為:%fn", *pFloat);//0.000000
	*pFloat = 9.0;
	printf("num的值為:%dn", n);//1091567616
	printf("*pFloat的值為:%fn", *pFloat);//9.000000
	return 0;
}

按照我們平常的思路,結果也許為:9,9.0,9,9.0,因為存進去什麼樣拿出來就應該是什麼樣,對於浮點型只要加上小數點後六位就好了,但是結果真的是這樣嗎?讓我們執行一下:

發現結果和我們的想法大相庭徑,那麼說明浮點型和整形在記憶體中的儲存方式是不同的!

接下來讓我們瞭解一下浮點型的儲存規則!

4. 浮點數儲存規則

根據國際標準IEEE 754,任意一個二進位制浮點數可以表示成下面的形式:

  • (-1)^S * M * 2^E
  • (-1)^S表示符號位,當S=0,V為正數;當S=1,V為負數。
  • M表示有效數位,大於等於1,小於2。
  • 2^E表示指數位。

4.1 浮點數的存

舉個栗子:

5.5
二進位制表示:101.1
解釋:(1*2^2 + 0*2^1 + 1*2^0).(1*2^(-1))
開始轉化:
101.1表示一個整數,所以S = 0;
而二進位制需要轉化成如下形式表示M:101.1 ->1.011;M >= 1 && M < 2
由於小數點向左移動兩位,所以指數位E為2;

表示結果:(-1)^0*1.011*2^2
               S   M     E

通過如上步驟,5.5就正確按照規則存到了記憶體中!

我們在觀察一下S E M在記憶體中是如何劃分的:

32位元浮點數:

對於32位元浮點數,最高位的1位是符號位S,接著的8位元是指數E,剩下的23位為有效數位M。

64位元浮點數:

對於64位元的浮點數,最高的1位是符號位S,接著的11位是指數E,剩下的52位為有效數位M。

IEEE 754的特殊規定:

有效數位M:

前面說過, 1≤M<2 ,也就是說,M可以寫成1.xxxxxx 的形式,其中xxxxxx表示小數部分。

IEEE 754規定,在計算機內部儲存M時,預設這個數的第一位總是1,因此可以被捨去,只儲存後面的xxxxxx部分。

比如儲存1.01的時候,只儲存01,等到讀取的時候,再把第一位的1加上去。這樣做的目的,是節省1位有效數位。以32位元浮點數為例,留給M只有23位,

將第一位的1捨去以後,等於可以儲存24位元有效數位。

指數E:

至於指數E,情況就比較複雜。

首先,E為一個無符號整數,這意味著,如果E為8位元,它的取值範圍為0255;如果E為11位,它的取值範圍為02047。

但是,我們知道,科學計數法中的E是可以出現負數的,所以IEEE 754規定,存入記憶體時E的真實值必須再加上一個中間數,對於8位元的E,這個中間數是127;對於11位的E,這個中間數是1023。

比如,2^10的E是10,所以儲存成32位元浮點數時,必須儲存成10+127=137,即10001001。

4.2 浮點數的取

瞭解了浮點數的儲存,那麼它將資料取出又是什麼樣的呢?

指數E從記憶體中取出可以分成三種情況:

E不全為0或不全為1:

這時,浮點數就採用下面的規則表示,即指數E的計算值減去12(或1023),得到真實值,再將有效數位M前加上第一位的1。

比如:

0.5(1/2)的二進位制形式為0.1,由於規定正數部分必須為1,即將小數點右移1位,則為1.0*2^(-1),在記憶體中為-1+127=126,表示為01111110,而尾數1.0去掉整數部分為0,補齊0到23位00000000000000000000000,

則其二進位制表示形式為:0 01111110 00000000000000000000000

E全為0:

這時,浮點數的指數E等於1-127(或者1-1023)即為真實值,

有效數位M不再加上第一位的1,而是還原為0.xxxxxx的小數。這樣做是為了表示±0,以及接近於0的很小的數位。

比如:

0.000000001 * 2 ^(-126),這是不是一個極小的數位,幾乎接近於0(正負取決於符號位s)。

E全為1:

這時,若有效數位M全為0(0/1 11111111 00000000000000000000000),表示±無窮大。

若有效數位M不全為0,則表示有效資料,是一個非常大的值。

5. 例題解答

int main()
{
	int n = 9;
	//二補數:0000 0000 0000 0000 0000 0000 0000 1001
	float* pFloat = (float*)&n;
	//浮點型儲存:0 00000000 00000000000000000001001
	//E為全零,E = 1 - 127 = -126
	//M不再加上1,M = 0.00000000000000000001001
	//正數,S = 0
	//(-1)^0 * 0.00000000000000000001001 * 2^(-126)
	//幾乎為0
	printf("n的值為:%dn", n);//儲存為整形,取出來也是按照整形的方式取:9
	printf("*pFloat的值為:%fn", *pFloat);//0.000000
	*pFloat = 9.0;
	//二進位制表示:1001.0
	//小數點右移三位:1.001 * 2 ^ 3
	//S = 0
	//M = 1.001
	//E = 3
    (-1)^0 * 1.001 * 2^3
	//浮點型儲存:0 01000010 00100000000000000000000
	printf("num的值為:%dn", n);//1091567616
	//認為記憶體中放的是有符號整數,符號位為0,正數
	//認為二進位制序列就是它的原碼
	printf("*pFloat的值為:%fn", *pFloat);//儲存為浮點型,取出來也是按照浮點型的方式取:9.000000
	return 0;
}

至此,浮點型的儲存方式,你明白了嗎?

6. 浮點型的精度探究(※)

此部分為浮點型儲存的拓展部分,內容為對浮點型資料之間的比較和浮點型精度的講解,由於這部分內容在實際學習時很少提及,故歸為拓展部分,有興趣的話可以瞭解一下。

(由於浮點數預設是double型別,使用float可能會出現告警,考慮到方便起見和double精度更高,所以以下內容均使用double型別)

6.1 浮點數的精度丟失

我們瞭解了浮點數的儲存方式之後,發現浮點數在儲存時有時是無法精確儲存的,是可能會有精度損失的,注意這裡的損失,不是一味的減少了,還有可能增多。浮點數本身儲存的時候,在計算不盡的時候,會採用“四捨五入”或者其他策略。

樣例:

int main()
{
	double x = 3.6;
	printf("%.50lfn", x);
	return 0;
}

執行結果:

6.2 浮點數之間如何比較

上述例子意味著,浮點數存入時資料會不一樣,這是否說明浮點數之間無法正常比較?事實勝於雄辯,接著探究:

int main()
{
	double x = 1.0;
	double y = 0.1;
	printf("%.50lfn", x - 0.9);
	printf("%.50lfn", y);
	if ((x - 0.9) == 0.1)
	{
		printf("==");
	}
	else
	{
		printf("!=");
	}
	return 0;
}

執行結果:

我們觀察結果,發現不僅儲存的內容發生了變化,而且直接用==相比較得出的結果也是不等於,那麼浮點數之間是如何比較的?應該如何理解?

C語言中浮點數應該進行範圍精度比較!!!

如圖:兩個浮點數只要進行比較時,它們的差值,只需要在-EPS ~ EPS的精度範圍內,那麼便認定兩個數相等!!!

同樣的,條件也可以寫成絕對值的形式:fabs((x - y) < EPSLION)。fabs是一個庫函數,計算變數的絕對值,相關標頭檔案為#include<math.h>

這裡的EPSILON是精度,精度分為系統提供的精度(float.h中定義)和自定義的精度:

自定義的精度:

#define EPS 0.00000000000001//自定義的精度,名字,數值可以自定義

系統提供的精度:

#define DBL_EPSILON//系統提供的精度,需要引標頭檔案
#include<float.h>

轉到定義觀察一下:

這個數小到什麼程度呢?1.0雖然加很多資料都不等於1.0,但是這個DBL_EPSILON是最小的。

使用精度後,再進行比較:

//#define EPS 0.00000000000001//自定義方案
#define DBL_EPSILON//系統提供的精度,是一個非常小的值
#include<stdio.h>
#include<float.h>
#include<math.h>
int main()
{
	double x = 1.0;
	double y = 0.1; 
	printf("%.50lfn", x - 0.9);
	printf("%.50lfn", y);
	//if (fabs((x - 0.9) - y) < EPS)//自定義
	if (fabs((x - 0.9) - y) < DBL_EPSILON)//系統
	{
		printf("==n");
	}
	else
	{
		printf("!=n");
	}
	return 0;
}

執行結果:

6.3 浮點數和0比較

對於兩個浮點數之間比較寫做x - y的形式,那麼對於浮點數和0比較就為x - 0,直接寫做x即可。

樣例:

#include<float.h>
#include<stdio.h>
#include<math.h>
int main()
{
	double x = 0.0;
	if (fabs(x) < DBL_EPSILON)//fabs(x - 0)
	//if(x > -DBL_EPSILON && x < DBL_EPSILON)
	{
		printf("yesn");//一個浮點數和0比較,只要保證它的絕對值在精度範圍內就相等
	}
	return 0;
}

執行結果:

但是,浮點數和零值比較要不要相等?x >= -DBL_EPSILON && x <= DBL_EPSILON)(fab(x) <= DBL_EPSILON)?

精度是引起x變化的最小值,但是精度 + x依舊能引起精度變化;

如果寫成fab(x) <= DBL_EPSILON;也就是fab(x) == DBL_EPSILON的形式,這時x就是精度;

double y + x != y;也就是加上x(精度)會引起變化,相當於double y + DBL_EPSILON != y;

但是y + 0.0 == y,y加上零值時,值是不會變的!!!

因為精度本身就會引起值的變化,只是很小而已,就不符合0的概念;

寫法比較矛盾,不建議寫上等號!!!

所以,浮點數和零值進行比較,只要保證浮點數的絕對值在精度範圍內,就判定浮點數和零值相等,且比較時,不建議加上等號。

7. 結語

到這裡本篇部落格到此結束,相信通過這兩篇文章,大家也可以對資料在記憶體中的儲存有了一定的認識,雖然這些知識並不會對程式設計能力有很大的提高,但是能擴大我們看待問題的視角,也算修煉了"內功"。

到此這篇關於C語言深入分析浮點型資料儲存的文章就介紹到這了,更多相關C語言資料儲存內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com