肖紅德
摘 要:為弄清浮點類型數據在應用中存在“異常”現象的原因,研究了浮點類型數據在內存中的存儲形式,得到與其在內存中存儲規范一致的結果。分析浮點類型數據在內存中存儲形式對應的理論區間并進行實驗驗證,得出浮點類型數據的精度(對于規范化浮點類型數據,float類型有效位數為6~8位,double類型有效位數為15~17位),進而對浮點類型數據在應用中的一些異常現象進行合理解釋,比如“大數吃小數”、輸出格式控制以及輸出結果與預期不一致等。通過對浮點類型數據在計算機內存中表達形式的理論分析和實驗驗證,實現對實際數據進行離散化處理,為計算機內存中的表示和相關計算帶來幫助。
關鍵詞:浮點類型;有效位數;存儲單元;數制轉換;不精確表示
DOI:10. 11907/rjdk. 182282
中圖分類號:TP301文獻標識碼:A文章編號:1672-7800(2019)004-0050-07
0 引言
數據在計算機內存中是以二進制存放的[1-3]。基本整型int類型在存儲單元中的存儲方式用整數補碼存放[1]。Visual c++6.0為每一個int型數據分配4個字節(32位)。對于整數補碼的求法有以下規定:一個正數的補碼是其二進制形式;一個負數的補碼,應先獲得其絕對值的二進制形式,然后對其后所有二進位按位取反,再加1[2]。十進制轉換R進制按照整數部分除R取余和小數部分乘R取整的方法進行[4,5]。在存放int類型數據的存儲單元中,最左面一位用來表示符號,如果該位為0,則表示數值為正;如果該位為1,則表示數值為負。
浮點類型數據研究主要圍繞以下問題進行:①浮點類型在內存中的表示形式規定;②浮點類型數據表示范圍;③浮點類型數據精度;④驗證浮點類型數據和具體計算結果是否一致。
對于問題①,浮點類型數據在內存中的存儲形式有相關規定[1,3,6-19],其存儲形式與整型數據在內存中的存儲形式不同。浮點類型分為單精度浮點類型(float類型)和雙精度浮點類型(double類型)兩種。Visual C++6.0編譯軟件為float類型數據分配4個字節,為double類型數據分配8個字節,數值以規范化二進制指數形式存放在存儲單元中。在存儲時,系統將浮點類型數據分成小數部分和指數部分,分別存放。不論是float類型數據還是double類型數據,存儲方式上都遵從IEEE規范,float類型數據遵從IEEE R32.24,而double類型數據遵從IEEER64.53[8]。
對于問題②,文獻[10]進行了較為詳細的研究和分析。
對于問題③,文獻[9,20]進行了相關浮點類型數據位數的理論計算和分析。
對于問題④,文獻[3,6]通過實驗驗證了浮點類型數據存儲格式在計算機內存中的表示形式。
在下文描述中,S表示符號位,E表示指數位,e表示指數部分的位數,M表示尾數部分,B表示指數部分的偏移量(其值為B=[2e-1-1])。
IEEE標準規定,無論是float類型數據還是double類型數據在存儲中都分為3個部分:①符號位S:float類型和double類型都占1位,0代表正,1代表負;②指數位E:float類型占8位,double類型占11位,用于存儲指數數據,并且采用移位存儲,指數采用移碼表示(原來的實際指數值加上一個固定值),該固定值為B=[2e-1-1](表示偏移量,e為指數部分比特長度,對于float類型,e=8,偏移量B=127;對于double類型,e=11,B=1 023)。指數位存儲的是一個無符號整數,所以對于float類型,指數位E的取值范圍為0~255,其真正取值范圍為-127~128,對于double類型,指數位E的取值范圍為0~2 047,其真正取值范圍為-1 023~1 024;③尾數部分M:float類型占23位,double類型占52位。尾數采用原碼表示,用二進制形式,整數部分為1,那么小數點前的1就沒有必要用一個比特位去存儲,默認已經存在,稱為“隱藏位”。所以規定尾數部分在存儲時舍去第一個1,只存儲小數點之后的數字,好處是對于float類型,只保存23位小數信息,加上舍去的1,可以用來表示24個有效信息;對于double類型,可以用52位尾數部分表示53個有效信息。
浮點類型在內存中由高地址到低地址分別存儲符號位S、指數位E和尾數部分M。指數位E還分為3種情況:①指數位E不全為0,不全為1。這是一種規范化的浮點數形式,此時就用正常的計算規則,指數部分E的真實值就是其字面值減去偏移量,尾數部分M的值要加上最前面省去的整數1;②指數位E全為0。需要分兩種情況進行處理,若尾數部分M全為0,則表示浮點數0,若尾數部分M不全為0,則表示非規范化浮點數形式,表示很小的浮點數,并且指數實際值為-(B-1)。對于float類型,指數實際值為-126。對于double類型,指數實際值為-1 022,尾數部分表示實際小數部分,整數部分為0(即沒有“隱藏位”1);③指數位E全為1。當尾數部分M全為0時,表示±無窮大(取決于符號位),當M不全為0時,表示該數不是一個數(NaN)。
本文主要研究C語言中浮點類型(包括單精度浮點類型float和雙精度浮點類型double)有效位數的計算、某個具體數值在內存中的存儲形式、其所能表示的數據范圍以及在使用過程中的“反常”現象。在程序驗證過程中,采用編譯軟件Visual C++6.0。
1 數值存儲形式查看
數值在內存中的存儲形式,已有相關驗證程序可以進行查看[3,6,9,11,12]。在Intel CPU 架構系統中,數據在內存中的存放方式為小端模式(低字節存在低地址中,高字節存在高地址中)[3,21]。本文設計一個專門用來顯示某個變量在內存中存儲形式以及按照指定形式進行輸出的函數。
具體實現過程為:先建立頭文件“show.h”,然后在頭文件中定義實現查看內存的函數displaymemory。
void displaymemory(void * a,int m,int n)
{
unsigned char *b=a,c,k[8]={1,2,4,8,16,32,64,128};?int i,j;?for(i=m-1;i>=0;i--)?{? ?c=*(b+i);? ?for(j=7;j>=0;j--)? ?{? ? ?printf("%2d",c/k[j]);? ? ?c=c%k[j];? ?}? ?printf("\n");?}
switch(n)
{
case 0:printf("%d\n",*(int *)a);break;
case 1:printf("%.20e\n",*(float *)a);break;
case 2:printf("%.20e\n",*(double *)a);break;
default:printf("Data error!\n");
}
}
其中,函數displaymemory 中3個形參含義如下:void *a表示數據在內存中存儲的起始地址;int m表示需要查看的內存地址字節數;int n表示用何種格式符輸出以地址a開始的數據,如果n為0表示以d格式符輸出整數,如果n為1表示以.20e格式符、指數形式輸出帶20位小數部分的float類型數據,如果n為2表示以.20e格式符、指數形式輸出帶20位小數部分的double類型數據。
2 浮點數范圍與有效位數
2.1 浮點類型范圍
田祎、樊景博[10]對浮點類型數據范圍進行了研究。本文主要按照規范化浮點數和非規范化浮點數各自取值范圍分別進行討論。
規范化浮點數能夠表示的數其絕對值最大值為:對于float類型,指數位E為11111110,其表示的十進制數為254-127=127,尾數部分M為全1,其對應數據為[1.M*2127],所對應的十進制數約為[3.402 82*1038];對于double類型,指數位為11111111110,其表示的十進制數為2 046-1 023=1 023,尾數部分M為全1,其對應數據為[1.M*21 023],其所對應的十進制數約為[1.797 693 134 862 315 7*10308]。規范化浮點數能夠表示的絕對值最小非零數為:對于float類型,指數位E為00000001,表示的十進制數為1-127=-126,尾數部分M為全0,對應數據為[1*2-126],其所對應的十進制數約為[1.175 494 350 822 287 5*10-38];對于double類型,指數位為00000000001,表示的十進制數為1-1 023=????? -1 022,尾數部分為全0,對應數據為[1*2-1022],其所對應的十進制數約為[2.225 073 858 507 201 2*10-308]。浮點類型數據0.0在內存中的表示形式為:指數位E和尾數部分M全為0。
非規范化浮點數指數位E為全0,尾數部分M不全為0。非規范化浮點數能夠表示的數其絕對值最大值為:對于float類型,指數位E為全0,尾數部分M為全1,此時沒有“隱藏位”1,指數部分的值為-126,其對應數據為[0.M*2-126],十進制數約為[1.175 494*10-38];對于double類型,指數位E為全0,尾數部分M為全1,此時沒有“隱藏位”1,指數部分的值為-1 022,其對應數據為[0.M*2-1 022],十進制數約為[2.225 073 858 507 200 9*10-308]。對于非規范化浮點數,其能夠表示的非零數的絕對值最小值為:對于float類型,指數位E為全0,尾數部分M最后一位為1,其余全為0,此時沒有“隱藏位”1,指數部分的值為-126,其對應數據為[1*2-23*2-126],十進制數約為[1.401 298 464 324 817 1*][10-45];對于double類型,指數位E為全0,尾數部分M最后一位為1,其余全為0,此時沒有“隱藏位”1,指數部分值為-1 022,其對應數據為[1*2-52*2-1 022],十進制數約為[4.940 656 458 412 465 4*10-324]。非規范化浮點數所表示的數都很小,并且其有效數字的位數可能為0。
由浮點類型數據有關形式可知,當指數位E全為1、尾數部分M全為0時,表示無窮大;當指數位E為全1、尾數部分M不全為0時,表示不是一個數(NaN)。通過驗證可知,對于float類型的數據,當數據范圍為[3.40282357e38,1.797693134862315e308]時,表示無窮大,驗證程序1如下:
#include
#include"show.h"
void main()
{
float b1=3.40282356e38,b2=3.40282357e38,b3=1.797693134862315e308;
displaymemory(&b1,4,2);
displaymemory(&b2,4,2);
displaymemory(&b3,4,2);
}
由驗證結果可知,b1為最大的規范化float類型數據,b2和b3在內存中的表示形式相同,即指數位E為全1,尾數部分M為全0。
對于double類型的數據,如果超過規范化雙精度浮點類型限制,編譯時會提示“constant too big”錯誤,其無窮大能夠表示的數值范圍無法驗證。
由驗證程序1可得出以下結論:float類型在計算過程中都是按照double類型進行處理的,如果表示的數超過了規范化float類型數據范圍并且在double類型數據范圍內,則按照float類型中的無窮大進行處理;如果超過了double類型限制,則visual c++6.0編譯系統認為輸入的數據太大,不能進行處理。
2.2 浮點類型有效位數
張宗杰、張明亮[9]從相對誤差角度對浮點數的有效位數進行了解讀,逯鴻友[20]對不同進制之間轉換位數的確定給出了理論推導。本文從尾數的取值范圍和相對誤差兩方面進行分析,對該問題進行解釋和說明。
浮點數在內存中的存放是離散的,而不是連續的,即對于每一個浮點數來說,其在內存中表示的一個浮點類型數據都對應一個區間。因此,浮點類型數據是有限個。由浮點數的存儲規范可知:對于非負float類型數據來說,其個數為[255*223];對于非負double類型數據來說,其個數為[2 047*252][8]。
對于規范化浮點類型,按照其能夠表示的二進制位數,可以計算出其最大相對誤差:對于float類型,當尾數部分M為0全時,取最大相對誤差值約為[0.5*2-23=2-24=][5.960 5e-08],當尾數部分M為全1時,取最小相對誤差值約為[0.5*0.5*2-23=2.980 2e-08];對于double類型,當尾數部分為全0時,取最大相對誤差值為[0.5*2-52=2-53],約為1.110 2e-16,當尾數部分M為全1時,取最小相對誤差值為[0.5*0.5*2-52],約為5.551 1e-17[8]。由相對誤差的計算結果可知,對于float類型,其有效數字位數最多為8,對于double類型,其有效數字位數最多為17。而對于非規范化浮點類型,其有效數字的位數是不同的,對于float類型,有效數字的位數為0~8,對于double類型,有效數字的位數為0~17,總體來說,有效數字的位數隨著尾數部分有效二進制位增加而增加。
對有效數字位數的確定,主要與相對誤差的大小有關。由相對誤差計算結果可知,對于float類型,相對誤差主要影響從最高位開始的數字的第8位和第9位數字,前面7位數字在進行四舍五入進位前都是準確的,有效數字的位數需要分以下3種情況進行判斷:①如果內存中表示為同一個float類型數據的取值范圍在第8位數字處相同,并且第9位數字取值范圍進行四舍五入后與第8位數字相同,則該float類型數據有8位有效數字;②如果內存中表示為同一個float類型數據的取值范圍在第8位數字處不相同,并且在同一個區域(0~4或者5~9),則該float類型數據有7位有效數字;③如果內存中表示為同一個float類型數據的取值范圍在第8位數字處不相同,并且其取值范圍跨越兩個不同區域(0~4或者5~9),則第8位數字取值范圍影響到第7位數字的變化,并且變化后的值不同,則該float類型數據有6位有效數字。
從以上判斷過程可知,對于float類型,其有效數字的位數為6~8位。對于double類型,通過類似分析可知,其有效數字的位數為15~17位。
2.3 規范化浮點類型數據表示區間
2.3.1 理論分析
有關規范化浮點類型數據表示區間的問題,在文獻[1,7-11]中有相關說明,主要圍繞浮點數不能精確表示一個數從離散角度進行了解釋。本文主要從浮點類型存儲形式對應的理論數據范圍著手進行研究。
使用Fmax表示float類型數據在內存中存儲形式表示數據的最大偏移程度,使用Dmax表示double類型在內存中存儲形式表示數據的最大偏移程度(對于給定的指數位E,無論是float類型還是double類型,其最大偏移程度表示為尾數部分M最后一位尾數為1時所表示數據的一半)。因此,Fmax=[0.5*2E-B-23=2E-B-24],Dmax=[0.5*2E-B-52=2E-B-53]。
浮點類型數據在內存中存儲形式的確定過程如下:首先需要得到對應的二進制形式,即將十進制浮點數轉換為24位二進制形式,然后再進行存儲。米保全[4]介紹了有關十進制轉換為二進制的技巧。對于整數部分,十進制整數轉換為二進制的規則為:除2求余,商為下次的被除數,先得到的余數為二進制的低位部分,后得到的余數為二進制的高位部分,直到商為0為止。對于小數部分,十進制整數轉換為二進制的規則為:乘2取整,小數部分為下次的被乘數,先得到的整數為二進制的高位部分,后得到的整數為二進制的低位部分,直到小數部分為0或者加上前面整數部分轉換的二進制位超過了指定位數的二進制為止(float類型為24位,double類型為53位)。最后計算結果,對于規范化float類型數據保留前面最高24位二進制數字,對于規范化double類型數據保留前面最高的53位二進制數字。多于指定位數的二進制部分需要進行向上進位或者舍棄處理(通過驗證程序2可知,按照就近原則靠攏,如果距離兩個數值相同,則采用進位和舍棄交替進行的方式處理)。
如果將非負浮點數在內存中的形式從1開始按照自然數形式進行編號,1對應內存中存儲形式為0的數據,2對應最小正浮點數,則對于float類型,當指數位取值小于255時,編號i取值范圍為[1,[255*223]]。通過驗證程序2可知,編號i所能表示數的范圍可以用開、閉區間進行表示:當i為奇數時,相應內存數據對應的實際范圍為閉區間,否則為開區間。
對于float類型,編號i與對應指數位E以及尾數部分M之間關系為:由于指數部分確定時,其尾數部分出現的可能情況為[223],是偶數,開閉區間交替出現,所以編號i的開閉情況與指數位E無關,只與尾數部分M的情況有關。如果把尾數部分M看作23位二進制整數,那么當尾數部分M為偶數時是閉區間,當尾數部分M為奇數時是開區間,即當23位尾數部分的最后一位為0時是閉區間,當其最后一位為1時是開區間。doulbe類型數據分析過程與此類似,不再贅述。
2.3.2 區間確定
由上述尾數與開閉區間之間關系以及內存中存儲形式表示數的最大偏移程度,對于內存中的任意存儲形式,其所對應數據范圍也就確定下來了。目前的問題是如何確定最大偏移程度。
對于浮點類型,當指數位確定時,其最大偏移程度是確定的。對于float類型,Fmax=[2E-B-24];對于double類型,Dmax=[2E-B-53]。當尾數部分不全為0時,內存中存儲數據對應的浮點數t前后最大偏移程度是相同的。當最后一位尾數部分為0時,表示的數據范圍為閉區間,即對于float類型為[t-Fmax,t+Fmax],對于double類型為[t-Dmax,t+Dmax];當最后一位尾數部分為1時,表示的數據范圍為開區間。當尾數部分為全0時,其最大偏移程度與兩個相鄰指數位部分有關。對于相鄰兩個指數位E'和E=E'+1:對于float類型,Fmax=[2E-B-24],數據前面的最大偏移量為0.5*Fmax,數據后面的最大偏移量為Fmax,即對于float類型數據t,其所能表示的區間為[t-0.5*Fmax,t+Fmax];對于double類型,Dmax=[2E-B-53],其所能表示的區間為[t-0.5*Dmax,t+Dmax],即相鄰兩個指數部分的最大偏移量相差1倍。
浮點數不能精確地表示一個數字,規范化float類型數據有6~8位有效數字,double類型數據有15~17位有效數字,在計算一個內存中存儲形式對應的浮點數表示區間時,一般情況下會與理論值有微小誤差。而且對于浮點數,vc++6.0在計算過程中都是按照double類型進行計算[1],而double類型所能表示的有效數字位數為15~17位,因此在表示float類型數據對應的理論區間時需要按照double類型的計算過程進行,即對于float類型數據的表示區間[a,b]或(a,b),對于a的表示,需要按照double類型數據對待,即只要區間[c,d]或(c,d)內的值表示double類型a的值即可,類似可以得到對于b的取值范圍為[e,f]或(e,f)。從而可以得到對應float類型數據的表示區間需要使用區間[c,f] (d,e)進行表示,即對于float類型,當尾數部分M不全為0、最后一位尾數部分為0時,float類型數據t的理論區間為 [t-Fmax-Dmax,t+Fmax+Dmax],當最后一位尾數部分不為0時為開區間,表示為(t-Fmax+Dmax,t+Fmax-Dmax),其中Fmax=[2E-B-24]為float類型下最大偏移量,Dmax=[2E-B-53]為對應double類型下最大偏移量;當尾數部分M為全0時,對于相鄰兩個指數位E'和E=E'+1,則對于指數部分為E的第一個數值(即尾數部分M為全0),float類型數據t的理論區間為[t-0.5*Fmax-0.5*Dmax,t+Fmax+Dmax],其中Fmax=[2E-B-24]為float類型下最大偏移量,Dmax=[2E-B-53]為對應double類型下最大偏移量。
因此,對于內存中存儲形式對應的float類型數據t,可以得出以下結論:
(1)當尾數部分M全為0時,內存中存儲形式對應數據t表示的數據區間為:
(2)當尾數部分M不全為0并且最后一位為0時,內存中存儲形式對應數據t表示的數據區間為:
(3)當尾數部分M不全為0并且最后一位為1時,內存中存儲形式對應數據t表示的數據區間為:
其中,Fmax=[2E-B-24]為float類型下的最大偏移量,Dmax=[2E-B-53]為對應double類型下的最大偏移量。
2.3.3 區間驗證
對于尾數部分全為0的float類型數據,比如float類型數據167 772 16在內存中進行存儲時,通過計算可知,其對應的二進制表示為1 00000000 00000000 00000000,即[224],在內存中的存儲形式為01001011 10000000 00000000 00000000。167 772 15.5距離167 772 15與167 772 16相同,對167 772 16進行向上進位處理。由式(1)可知,其對應的理論區間為[167 772 16-0.5*Fmax-0.5*Dmax,167 772 16+Fmax+Dmax],其中Fmax=[2151-127-24]=1,Dmax=[21 047-1 023-53]=1.862 6e-09,即理論區間值為[167 772 16-0.5*1-0.5*1.862 6e-09, 167 772 16+1.0+1.862 6e-09]。驗證程序2如下:
#include
#include"show.h"
void main()
{
float b1=16777216-0.5-9.3133e-10,b2=16777216-0.5-9.3132e-10,
b3=16777216+1.0+1.8626e-09,b4=16777216+1.0+1.8627e-09;
displaymemory(&b1,4,1);
displaymemory(&b2,4,1);
displaymemory(&b3,4,1);
displaymemory(&b4,4,1);
}
從驗證結果可知,b2和b3輸出的是float類型數據??? 167 772 16,而b1和b4輸出的不是該數據,從而驗證了上文理論結果是成立的。類似可以驗證167 772 19距離??? 167 772 18和167 772 20相同,對167 772 20進行向上進位處理,167 772 21距離167 772 20和167 772 22相同,對?? 167 772 20進行舍棄處理等。
對于尾數部分不全為0的float類型,比如,對于float類型數據838 860 9,通過計算可知其在內存中的存儲形式為01001011 00000000 00000000 00000001,符號位S為0,表示正數,指數位E存放的數對應整數值為150,尾數部分M最后一位為1,其余全為0。則其在內存中的存儲值為:[1*2150-127+1*2-23*2150-127]=838 860 9。由式(3)可知,其能夠表示的數據范圍通過計算可知為(838 860 9-Fmax+Dmax,838 860 9+Fmax-Dmax),其中Fmax=[2150-127-23-1]=0.5,Dmax=[21 046-1023-52-1]=9.313 2e-10,即(838 860 9-0.5-9.313 2e- 10,838 860 9+0.5+9.313 2e-10)。與驗證程序2類似,可以驗證該區間。
2.4 非規范化浮點類型數據有效數字位數
非規范化浮點類型有效數字位數與尾數部分高位處開始出現的1的位置有關,從1開始到尾數部分結束的位數決定了對應十進制有效數字位數。總體來說,從尾數部分高位處開始出現1的位置越靠前,有效數字的位數就越多,非規范化浮點類型表示非常小的數字。
比如,對于float類型數據,其在內存中的存儲形式為:00000000 00000000 00000000 00000001,即符號位S為0表示正,指數位E為全0表示真實指數值為-126,尾數部分M只有最后一位為1,其余全為0,該存儲形式通過計算可知其理論值為: [1*2-23*2-126=2-149],而對該數值的十進制形式計算比較麻煩,因此,在下文驗證過程中,采用對指定float類型數據存儲位置賦值的方式對該數據賦值,然后輸出該最小正float類型數據的指數形式,從而確定最小正float類型在內存中的表示形式及其所對應的十進制指數形式。驗證程序3如下:
#include?
typedef?struct?FP_SINGLE
{
??unsigned?__int32?M:23;
??unsigned?__int32?E:8;
??unsigned?__int32?S:1;
}?fp_single;
void?main()
{
float?x;
fp_single?*?fp_s=&x;
fp_s->S=0;
fp_s->E=0;
fp_s->M=1;
??printf("float最小正非規范數:%le \n",x);
}
從運行結果可知,最小正float類型所能表示的非規范化數據為1.4012984643248171e-045。由式(3)可知,該運行結果表示的是一個范圍,其最大偏移程度為該數的一半。與驗證程序2類似,將b1、b2、b3、b4分別初始化為:7.00649232162408613e-46、7.00649232162408614e-46、2.10194769648722545e-45、2.10194769648722546e-45,可以驗證該理論區間。
由驗證結果可知,b1和b4在內存中存儲的是不同的數值,而b2和b3在內存中存儲的是同一個值1.4012984643248171e-045,而b2-b3范圍內的值與內存中存儲的值相比,有效數字位數可能為0。
由上述驗證結果可以得出以下結論:對于float類型數據,其非規范化數據有效數字的位數可能為0,總體來說,隨著非規范化數值增大,其有效數字的位數也逐漸增多,直到增加到6~8位為止,即其有效數字的位數在0~8位之間。對于double類型數據,其非規范化數據有效數字的位數也有類似結論,即其有效數字位數在0~17位之間。
3 浮點數使用注意問題
3.1 數據丟失與不能精確比較
對于浮點數在計算中的丟失現象,杜叔強等[12,13]給出了相關建議。本文主要從float類型在計算過程中以double類型進行計算的角度進行解釋和分析。
比如,float類型變量a=123 456 789 00,b=50;則a+b的結果不是123 456 789 50,在內存中,a和b的存儲形式都是01010000 00110111 11110111 00000111,其值為???? 123 456 788 48,因為a和b在計算時有效數字位數多于9位,不能精確存儲。該數123 456 789 00在內存中存儲的值為123 456 788 48,該值加上50之后,沒有超過最大偏移量,還是123 456 788 48。通過計算可知,float類型數據123 456 788 48在內存中存儲的指數位E為160,Fmax=[2160-127-23-1=29=512],Dmax=[21056-1023-52-1=2-20=][9.5367e-07],由式(3)可知,其表示的范圍為開區間(123 456 788 48-Fmax+Dmax,12345678848+Fmax-Dmax),即(123 456 788 48-512+9.5367e-07,123 456 788 48+512- 9.5367e-07),如果計算結果在該區間,則都會以???????????? 123 456 788 48進行存儲和顯示。與驗證程序2類似的過程,可以驗證該區間。
由該現象可以得到一個結論:盡量不要使用兩個差別較大的浮點類型數據進行運算,否則會出現“大數吃小數”的現象。比較兩個數的差別,以較小數是否大于較大數的最大偏移量為準,如果較小數大于較大數的最大偏移量,則認為兩個數差別不大,可以直接進行運算,否則,要盡量避免直接進行運算。
類似可以驗證,只要一個數加上數據的絕對值小于該數在內存中的最大偏移量,無論先后加上多少個該類數據,都是該數據本身。驗證程序4如下:
#include
#include"show.h"
void main()
{
float b=12345678848,b1;
int? i;
displaymemory(&b,4,1);
b1=b;
for(i=0;i<100;i++)b1=b1+500;
displaymemory(&b1,4,1);
}
從計算結果來看,b和b1輸出結果相同,因此要盡量避免進行類似運算,即盡量避免將一個較大數和一個較小數(較小數小于較大數在內存中對應存儲形式的最大偏移量)進行運算。
如果想讓較小數起作用,則需要多個較小數在同一個表達式中出現,因為float類型數據是按照double類型數據進行計算的,double類型數據規范化形式有15~17位有效數字,可以將部分較小的數據保留下來,最后再將計算得到的double類型數據自動轉換為float類型數據進行保存。驗證程序5如下:
#include
#include"show.h"
void main()
{
float b=12345678848,b1;
int? i;
displaymemory(&b,4,1);
b1=b+500+500;
displaymemory(&b1,4,1);
}
從計算結果來看,b和b1輸出結果不同,原因是對于b1的計算將b與多個較小數直接加在一起,在運算時按照double類型進行計算,因此能夠保留較多有效位數,只要多個較小數加在一起超過了較大數的最大偏移量,就可以得到改變后的數。
由于浮點數表示的數據不精確,一定范圍內數值在內存中存儲的形式相同,因此應該盡量避免兩個接近的浮點數進行相等和不相等比較。比如兩個float類型的變量a=12345678900、b=12345678950,在進行a==b的條件判斷時結果為真,因為a和b在內存中的存儲形式相同,認為這兩個變量相等。驗證程序6如下:
#include
#include"show.h"
void main()
{
float a=12345678900,b=12345678950;
if(a==b)printf("a==b\n");
else printf("a!=b\n");
}
從運行結果來看,會輸出“a==b”,因為對于浮點類型變量a和b,其在內存中的存儲形式相同,因此條件“a==b”是成立的。所以,對于浮點類型變量,應避免進行相等和不相等的判斷。
3.2 不同類型數據輸出時出現反常現象
C語言中常用的數據輸出格式符有d、c、f[1],對于不同數據類型需要使用不同格式符進行輸出,如果不小心用了不該用的格式符,則輸出結果會與預期結果不同。字符型數據可以按照c格式符或者d格式符進行輸出,分別按照字符形式或者對應的十進制整數形式進行輸出;基本整型數據可以按照d格式符輸出其十進制整數形式進行輸出;f格式符用來輸出浮點類型數據的十進制小數形式,如果要輸出指數形式,則以e格式符進行輸出,f格式符和e格式符都默認輸出6位小數部分。
如果數據輸出時沒有按照其正常格式符進行輸出,則輸出結果按照對應格式符存儲形式要求進行輸出。比如:double a=2.5,printf(“%d\n”,*(int *) &a),輸出結果是0,因為2.5在內存中按照double類型進行存儲,占8個字節,其在內存中的存儲形式為01000000 00000100 00000000 00000000 00000000 00000000 00000000 00000000,而輸出時按照后4個字節整數對應的存儲形式規則進行讀取。驗證程序7如下:
#include
#include"show.h"
void main()
{
double a=2.5;
displaymemory(&a,8,2);
printf("%d\n",a);
}
如果把整數按照f格式符進行輸出,將整數的地址按照浮點類型地址格式進行讀取,則讀取過程中會按照浮點類型的規則進行處理。比如:int a=655 36,printf(“%e\n”,a),變量a在內存中的存儲形式為00000000 00000001 00000000 00000000。單精度浮點類型數據的輸出結果是9.1835496157991212e-041,而雙精度浮點類型(因為f或e格式符默認將相應參數看作雙精度浮點類型數據進行輸出)輸出結果是5.597333e-308。由前面浮點類型在內存中存儲形式的規定可知:如果按照單精度浮點類型數據的處理方式進行處理,該存儲形式對應的單精度浮點數是一個非規范化浮點數;如果按照雙精度浮點類型數據的處理方式進行處理,需要變量a的地址和其前面4個字節的地址作為雙精度浮點類型數據在內存中的存儲形式進行處理,由于變量a的地址前面4個字節存儲形式不固定,因此,得到雙精度浮點類型數據在不同運行環境下一般是不同的,其測試存儲形式如下:00000000 00010010 11111111 11000000 00000000 00000001 00000000 00000000。驗證程序8如下:
#include
#include"show.h"
void main()
{
int a=65536;
displaymemory(&a,8,2);
printf("%e\n",a);
}
因此,對于不同數據類型,需要使用其對應的格式符進行輸出格式控制,否則會按照對應輸出格式的數據進行處理和輸出。
4 結語
不同數據類型在計算機內存中的存放方式不同,導致一些與整型類型不一樣的用法,比如數據有效位數限制、不能精確比較、“大數吃小數”等問題出現。只有了解不同數據的存儲長度和存儲形式,才能理解為什么有的計算結果與理論計算結果不同。杜叔強等[12,13]、周冠方[15]給出了浮點數使用注意事項。本文給出了浮點類型數據在內存中不同形式對應的理論取值區間,并通過實驗驗證了該區間的存在,在使用過程中需要根據遇到的不同情況加以處理。
由于浮點類型數據的有效位數有限(float類型為6~8位,double類型為15~17位),如果想得到更多有效位數,比如對于π值的計算,想要得到小數點后100位有效數字,則按照現有數據類型無法得到,需要定義新的能夠表示更多有效位數的數據類型才能進行。
參考文獻:
[1] 譚浩強. C程序設計(第四版)[M]. 北京:清華大學出版社, 2010.
[2] 向萬里,王智勇. C語言程序設計中關于補碼的幾個問題的探討[J]. 甘肅聯合大學學報:自然科學版,2008(S1):6-8.
[3] 吳艷婷,方賢進. 數據在計算機內存中的存儲形式及實驗驗證[J]. 安慶師范學院學報:自然科學版,2016,22(4):152-154.
[4] 米保全. 基于計算機中進制的轉換技巧[J]. 電子技術與軟件工程,2018(1):126.
[5] 楊翠芳. 淺談計算機基礎課程中數制的轉換問題[J]. 電子制作, 2014(18):72.
[6] 常玉紅,楊秀華. 巧用C語言指針驗證數據的存儲方式[J]. 電腦知識與技術,2007,3(14):393-397.
[7] 周恒忠. C語言實型數據的編碼和存儲[J]. 皖西學院學報,2007,23(5):19-21.
[8] ZURAS D,COWLISHAW M,AIKEN A.IEEE standard for floating-point arithmetic[C].? IEEE Std 754-2008,2008:1-70.
[9] 張宗杰,張明亮. C語言中浮點數的存儲格式及其有效數字位數[J]. 計算機與數字工程, 2006,34(1):84-86.
[10] 田祎,樊景博. C語言中浮點數的表示范圍淺析[J]. 軟件工程,2016,19(4):8-10.
[11] 王力. 科學計算程序語言的浮點數機制研究[J]. 計算機科學,2008,35(4):285-287.
[12] 杜叔強. 淺析C語言中的浮點數[J]. 蘭州工業學院學報,2010,17(5):26-28.
[13] 杜叔強,施武祖. 浮點數用法分析[J]. 蘭州工業學院學報,2012,19(3):51-53.
[14] 吳菊鳳,陳雪梨. 數制轉換過程中小數的“有限-無限”現象[J]. 紹興文理學院學報:自然科學版, 2011,31(1):16-19.
[15] 周冠方C語言中浮點數精度問題分析[J]. 湖北工業職業技術學院學報, 2015(3):97-99.
[16] 程裕強. 編程語言中浮點數精度丟失問題[J]. 計算機安全,2013(6):59-61.
[17] 陳愛民,毛莉珍.? Turbo C中兩個浮點數問題分析[J]. 寧德師范學院學報:自然科學版, 2013,25(4):370-372.
[18] 李偉,余森,門佳. 浮點數存儲精度丟失問題——由學生提問所引發的思考[J]. 濮陽職業技術學院學報,2015(3):151-153.
[19] 程寧,崔凱. C++浮點型數據存儲格式研究[J]. 南陽師范學院學報,2010,9(9):59-62.
[20] 逯鴻友. 關于數制轉換中轉換位數的確定問題[J]. 牡丹江師范學院學報:自然科學版, 2000(4):20-21.
[21] BAIDU. Little-endian[EB/OL].http://baike.baidu.com/view/2368412. htm.
(責任編輯:何 麗)