王兆華
摘要:針對(duì)高校本科生課程《C語(yǔ)言程序設(shè)計(jì)》中有關(guān)浮點(diǎn)數(shù)數(shù)據(jù)類型的認(rèn)識(shí)和使用中出現(xiàn)的問(wèn)題,學(xué)生存在對(duì)浮點(diǎn)數(shù)的認(rèn)知不夠清晰,對(duì)Visual C++2010環(huán)境下有關(guān)浮點(diǎn)數(shù)的相關(guān)計(jì)算結(jié)果存在各種困惑。根據(jù)多年的教學(xué)經(jīng)驗(yàn),查閱相關(guān)書(shū)籍和IEEE754標(biāo)準(zhǔn),論文分析Visual C++2010環(huán)境下浮點(diǎn)型數(shù)據(jù)的存儲(chǔ)形式,闡述了有關(guān)浮點(diǎn)數(shù)相關(guān)幾個(gè)重要知識(shí)點(diǎn)的理解。文中引入計(jì)算思維的指導(dǎo)思想,采用從現(xiàn)象到本質(zhì),從理論到實(shí)踐來(lái)逐步解決問(wèn)題。實(shí)踐證明,該方法取得的了較好的學(xué)習(xí)效果,夯實(shí)了學(xué)生對(duì)基礎(chǔ)知識(shí)的掌握和正確應(yīng)用。
關(guān)鍵詞:浮點(diǎn)數(shù);IEEE754;存儲(chǔ)形式;Visual C++2010環(huán)境;計(jì)算思維
中圖分類號(hào):TP311? ? ? ? 文獻(xiàn)標(biāo)識(shí)碼:A
文章編號(hào):1009-3044(2020)23-0041-04
Abstract: In view of the problems in the understanding and use of floating point data types in the undergraduate course C Language Programming, students are not clear enough about Floating point number, and they are confused about the calculation results of Floating point data in the Visual C++2010 environment. According to years of teaching experience, consult relevant books and IEEE754 standard, the paper analyzes the storage form of floating point number in Visual C++2010 environment, and expounds the understanding of several important knowledge points related to floating point Numbers. This paper introduces the guiding ideology of computational thinking and solves the problem step by step from phenomenon to essence and from theory to practice. The practice has proved that the method has achieved a good learning effect .The students' mastery and correct application of basic knowledge are consolidated.
Key words:floating point number; IEEE754; storage form; visual C++2010 environment; computational thinking;
1 引言
C語(yǔ)言是最受歡迎、最重要的和最流行的編程語(yǔ)言之一?!禖語(yǔ)言程序設(shè)計(jì)》課程是大部分大學(xué)本科理工類學(xué)生的一門(mén)必修課。C語(yǔ)言有豐富的數(shù)據(jù)類型,在介紹數(shù)據(jù)類型一章會(huì)介紹基本類型中的整型、字符型、浮點(diǎn)型。在這三種基本類型中浮點(diǎn)型是課程教學(xué)的重點(diǎn)和難點(diǎn),根據(jù)多年的教學(xué)經(jīng)驗(yàn)積累,筆者深入研究了Visual C++2010環(huán)境下浮點(diǎn)型數(shù)據(jù)的存儲(chǔ)形式,梳理教學(xué)重點(diǎn)和難點(diǎn),引入計(jì)算思維的思想,使得學(xué)生更容易理解浮點(diǎn)數(shù)的存儲(chǔ)形式。
周以真教授認(rèn)為:計(jì)算思維(Computational Thinking)是運(yùn)用計(jì)算機(jī)科學(xué)的基礎(chǔ)概念進(jìn)行問(wèn)題求解、系統(tǒng)設(shè)計(jì)以及人類行為理解等涵蓋計(jì)算機(jī)科學(xué)之廣度的一系列思維活動(dòng)[1]。它包括了涵蓋計(jì)算機(jī)科學(xué)之廣度的一系列思維活動(dòng)。當(dāng)我們必須求解一個(gè)特定的問(wèn)題時(shí),首先會(huì)問(wèn):解決這個(gè)問(wèn)題有多么困難?怎樣才是最佳的解決方法?計(jì)算機(jī)科學(xué)根據(jù)堅(jiān)實(shí)的理論基礎(chǔ)來(lái)準(zhǔn)確地回答這些問(wèn)題。表述問(wèn)題的難度就是工具的基本能力,必須考慮的因素包括機(jī)器的指令系統(tǒng)、資源約束和操作環(huán)境。
本文的思路是首先通過(guò)實(shí)踐發(fā)現(xiàn)浮點(diǎn)數(shù)計(jì)算中存在的問(wèn)題,為什么結(jié)果不準(zhǔn)確?為什么會(huì)存在一些錯(cuò)誤判斷。要想弄清楚這些問(wèn)題,需要清楚理論知識(shí)和計(jì)算機(jī)的操作環(huán)境,以及計(jì)算機(jī)計(jì)算能力的限制。
2 拋出問(wèn)題
在使用浮點(diǎn)數(shù)的時(shí)候,我們發(fā)現(xiàn)一些問(wèn)題:
(1)一個(gè)浮點(diǎn)數(shù)不能準(zhǔn)確的輸出,為什么?程序代碼如下:
#include
int main()
{
float fa=56.982f;
printf("fa=%f\n",fa);
return 0;
}
這個(gè)程序的執(zhí)行結(jié)果是:fa=56.981998,而不是fa=56.982。
(2)一個(gè)浮點(diǎn)數(shù)與一個(gè)極小的浮點(diǎn)數(shù)求和,結(jié)果不正確,為什么?程序代碼如下:
#include
int main()
{
float fa=2.5f;
fa=fa+0.0000001f;
printf("fa=%f\n",fa);
return 0;
}
這個(gè)程序的執(zhí)行結(jié)果是:fa=2.500000,而不是fa=2.5000001。
(3)數(shù)學(xué)上兩個(gè)不相等的浮點(diǎn)數(shù)程序卻認(rèn)為是相等的,為什么?程序代碼如下:
#include
int main()
{
float fa=1.00000001f,fb=1.00000002f;
if(fa==fb)
printf("兩個(gè)數(shù)相等?。躰");
return 0;
}
這個(gè)程序的執(zhí)行結(jié)果是:“兩個(gè)數(shù)相等!”,顯然是錯(cuò)誤的。
筆者所給的三個(gè)程序的運(yùn)行環(huán)境是Win7+Visual C++2010,因此帶著這幾個(gè)疑問(wèn),有必要深入研究一下在這樣的編譯和運(yùn)行環(huán)境下浮點(diǎn)數(shù)的存儲(chǔ)形式。
3 IEEE754標(biāo)準(zhǔn)
浮點(diǎn)數(shù)是指小數(shù)點(diǎn)位置根據(jù)需要可以浮動(dòng)的數(shù)據(jù),浮點(diǎn)數(shù)的一般表示形式為:N=RE×D,其中N為浮點(diǎn)數(shù),R稱為基數(shù),E稱為階碼,D稱為尾數(shù)。如圖1所示。
R為一常數(shù),與尾數(shù)的基數(shù)相同,一般為2、8、或16,在一臺(tái)計(jì)算機(jī)中,所有數(shù)據(jù)的R都是相同的,不需要在每個(gè)數(shù)據(jù)中表示出來(lái)。任意一個(gè)二進(jìn)制浮點(diǎn)數(shù)N可以表示為:N=2E×D。
在浮點(diǎn)數(shù)表示中,即使數(shù)據(jù)字長(zhǎng)相同時(shí),不同的計(jì)算機(jī)可能選用不同的格式、不同的階碼與尾數(shù)位數(shù)及其編碼,從而導(dǎo)致不同計(jì)算機(jī)之前浮點(diǎn)機(jī)器數(shù)差異很大,不利于軟件移植,為此,美國(guó)電氣電子工程師協(xié)會(huì)于1985年提出了浮點(diǎn)機(jī)器數(shù)IEEE754標(biāo)準(zhǔn),并得到廣泛應(yīng)用[2]。在軟件中IEEE 754以浮點(diǎn)庫(kù)的形式實(shí)現(xiàn),在硬件(如許多CPU和FPU)中的指令中實(shí)現(xiàn)。 第一個(gè)實(shí)現(xiàn)IEEE 754-1985草案的集成電路是Intel 8087。
當(dāng)今流行的計(jì)算機(jī)幾乎都采用IEEE754浮點(diǎn)數(shù)標(biāo)準(zhǔn),在這個(gè)標(biāo)準(zhǔn)中,每個(gè)浮點(diǎn)數(shù)均由數(shù)符S、階碼E、尾數(shù)M三部分組成。用三元組{S,E,M}表示一個(gè)數(shù)N,如圖2所示。
IEEE754標(biāo)準(zhǔn)規(guī)定了四種浮點(diǎn)數(shù)的表示方式:?jiǎn)尉_度(32位)、雙精確度(64位)、延伸單精確度(43比特以上,很少使用)與延伸雙精確度(79比特以上,通常以80比特實(shí)做)。C語(yǔ)言中float和double浮點(diǎn)型分別對(duì)應(yīng)的是單精度和雙精度浮點(diǎn)數(shù),下面以單精度浮點(diǎn)數(shù)為例介紹浮點(diǎn)數(shù)的存儲(chǔ)形式,如圖3所示:?jiǎn)尉雀↑c(diǎn)數(shù)(32位):N共32位,其中S占1位,E占8位,M占23位。
單精度浮點(diǎn)數(shù)根據(jù)階碼和尾數(shù)的取值情況分為:0,非規(guī)格化數(shù),規(guī)格化數(shù),無(wú)窮和NaN(Not a Number,非數(shù))。如表1所示。
浮點(diǎn)數(shù)的尾數(shù)采用補(bǔ)碼形式存儲(chǔ),階碼采用移碼形式存儲(chǔ)。下面需要清楚什么是補(bǔ)碼和移碼。
4 原碼、反碼、補(bǔ)碼、移碼的概念
要想了解浮點(diǎn)數(shù)在計(jì)算機(jī)中的存儲(chǔ)學(xué)生務(wù)必清楚原碼、反碼、補(bǔ)碼、移碼相關(guān)概念。原碼、補(bǔ)碼,反碼是把符號(hào)和數(shù)值一起編碼的表示方法,是機(jī)器數(shù)的表示形式。
(1)原碼:是用“符號(hào)碼+二進(jìn)制絕對(duì)值”表示的機(jī)器碼。符號(hào)碼用0表示正,用1表示負(fù)。
(2)反碼:引入反碼是為了求負(fù)數(shù)的補(bǔ)碼。正數(shù)的反碼與原碼相同,負(fù)數(shù)的反碼除符號(hào)位不變外,其余各位取反。
(3)補(bǔ)碼:補(bǔ)碼的引入是為了在計(jì)算機(jī)中把減法運(yùn)算轉(zhuǎn)換為加法運(yùn)算。使減法運(yùn)算變得簡(jiǎn)單,在計(jì)算機(jī)中容易實(shí)現(xiàn)。正數(shù)的補(bǔ)碼與原碼相同,負(fù)數(shù)的補(bǔ)碼在反碼的末尾加1。
(4)移碼:又稱為增碼或偏碼,在計(jì)算機(jī)中主要用于表示浮點(diǎn)數(shù)的階碼,而階碼是整數(shù),因此階碼僅需要定點(diǎn)證書(shū)編碼法。移碼實(shí)質(zhì)是在真值X基礎(chǔ)上加一個(gè)固定正整數(shù)(稱為偏置值),把真值映射到一個(gè)正數(shù)域,相當(dāng)于在數(shù)軸上將真值X向正方向平移一段距離,這也是該編碼命名為“移碼”的來(lái)由。對(duì)于二進(jìn)制純整數(shù),移碼與真值得映射式為:[X]移=偏置值+X。偏置值選取的原則是使真值X中絕對(duì)值最大的負(fù)數(shù)對(duì)應(yīng)的編碼量值為0,因此偏置值一般取2n。當(dāng)偏置值取2n時(shí),移碼與真值得映射式為:[X]移=2n+X (-2n <=X<=(2n-1))。
純整數(shù)偏置值2n移碼與補(bǔ)碼之間的關(guān)系為:同一純整數(shù)真值的偏置值2n移碼與補(bǔ)碼,它們的有效數(shù)值位相同,符號(hào)位互反。
清楚補(bǔ)碼和移碼編碼之后,對(duì)于多個(gè)字節(jié)的浮點(diǎn)數(shù),在存儲(chǔ)器中應(yīng)該怎么存儲(chǔ)呢?
5 計(jì)算機(jī)存儲(chǔ)的大端法和小端法
對(duì)于多字節(jié)的程序?qū)ο?,必須建立兩個(gè)規(guī)則:這個(gè)對(duì)象的地址是什么,以及在存儲(chǔ)器中如何排列這些字節(jié)。在幾乎所有的機(jī)器上,多字節(jié)對(duì)象都被存儲(chǔ)為連續(xù)的字節(jié)序列,對(duì)象的地址為所使用在字節(jié)最小的地址。例如:假設(shè)在VC++2010中編譯的C程序中一個(gè)int的變量x的地址是a,那么x的4個(gè)字節(jié)將分別被存儲(chǔ)在存儲(chǔ)器地址為:a、a+1、a+2、a+3的四個(gè)存儲(chǔ)單元。
某些機(jī)器選擇在存儲(chǔ)器中按照從最低有效字節(jié)到最高有效字節(jié)的順序存儲(chǔ)對(duì)象,而另一些機(jī)器則按照從最高有效字節(jié)到最低有效字節(jié)的順序存儲(chǔ)。最低有效字節(jié)在前面的方式,稱為“小端法(little endian)”。最高有效字節(jié)在最前面的方式,稱為“大端法(big endian)”。大多數(shù)Intel兼容機(jī)都采用“小端法”的規(guī)則。大多數(shù)IBM和Sun Microsystems的機(jī)器都采用“大端法”的規(guī)則。IBM和Sun制造的個(gè)人計(jì)算機(jī)使用的是Intel兼容的處理器,因此使用的就是“小端法”。許多比較新的微處理器使用“雙端法(bi-endian)”,也就是可以把它們配置成作為大端或者小端的機(jī)器運(yùn)行[3]。
示例:變量x類型為int,位于地址a,int型數(shù)據(jù)在VC++2010中占32字節(jié),假如它的十六進(jìn)制為0x01234567。采用“大端法”(圖4)和“小端法”(圖5)的存儲(chǔ)順序分別如圖4和圖5所示。
可以通過(guò)下面的程序確認(rèn)機(jī)器所采用的存儲(chǔ)方式,代碼編譯運(yùn)行平臺(tái)Win32 + VC++2010,機(jī)器為聯(lián)想ThinkPad T430,CPU型號(hào)是Intel酷睿i52520M。
#include
int main()
{
int x=0x01234567;
char *px=(char *)&x;
if(*px==0x01)? //判斷高字節(jié)0x01是否保存在低地址
printf("大端法\n");
else
printf("小端法\n");
return 0;
}
程序執(zhí)行結(jié)果:“小端法”。
清楚了當(dāng)前計(jì)算機(jī)采用的是“小端法”之后,需要通過(guò)程序來(lái)驗(yàn)證浮點(diǎn)數(shù)的具體存儲(chǔ)形式。
6 Visual C++2010環(huán)境下單精度浮點(diǎn)數(shù)的具體存儲(chǔ)形式
下面以具體的數(shù)為例,在編譯環(huán)境下驗(yàn)證浮點(diǎn)數(shù)的存儲(chǔ)形式。
(1)十進(jìn)制0.5
第一步:把十進(jìn)制0.5轉(zhuǎn)換為二進(jìn)制結(jié)果為(0.5)10=(0.1)2。
第二步:把(0.1)2規(guī)格化為2e×D 即2-1× (1)2。
第三步:分別求解S,M,e,E。S=0, M=[000 0000 0000 0000 0000 0000]2,e=-1,E=e+127=126。
第四步:求解機(jī)器碼(實(shí)際存儲(chǔ)形式):[0.5]10= [0011 1111 0000 0000 0000 0000 0000 0000]2。
第五步:用十六進(jìn)制表示機(jī)器數(shù):3F000000。
(2)十進(jìn)制1.25
第一步:把十進(jìn)制1.25轉(zhuǎn)換為二進(jìn)制結(jié)果為(1.25)10=(1.01)2。
第二步:把(1.01)2規(guī)格化為2e×D 即20× (1.01)2。
第三步:分別求解S,M,e,E。S=0, M=[010 0000 0000 0000 0000 0000]2,e=0,E=e+127=127。
第四步:求解機(jī)器碼(實(shí)際存儲(chǔ)形式):[1.25]10= [0011 1111 1010 0000 0000 0000 0000 0000]2。
第五步:用十六進(jìn)制表示機(jī)器數(shù):3FA00000。
(3)十進(jìn)制-1.25
第一步:把十進(jìn)制-1.25轉(zhuǎn)換為二進(jìn)制結(jié)果為(-1.25)10=(-1.01)2。
第二步:把(-1.01)2規(guī)格化為2e×D 即20 ×(-1.01)2。
第三步:分別求解S,M,e,E。S=-1, M=[010 0000 0000 0000 0000 0000]2,e=0,E=e+127=127。
第四步:求解機(jī)器碼(實(shí)際存儲(chǔ)形式):[-1.25]10= [1011 1111 1010 0000 0000 0000 0000 0000]2。
第五步:用十六進(jìn)制表示機(jī)器數(shù):BFA00000。
(4)十進(jìn)制124.25
第一步:把十進(jìn)制124.25轉(zhuǎn)換為二進(jìn)制結(jié)果為(124.25)10=( 1111100.01)2
第二步:把( 1111100.01)2規(guī)格化為2e×D 即26 ×( 1.11110001)2。
第三步:分別求解S,M,e,E。S=0, M=[111 1000 1000 0000 0000 0000]2,e=6,E=e+127=133。
第四步:求解機(jī)器碼(實(shí)際存儲(chǔ)形式):[124.25]10= [0100 0010 1111 1000 1000 0000 0000 0000]2。
第五步:用十六進(jìn)制表示機(jī)器數(shù):42F88000。
程序驗(yàn)證(代碼編譯運(yùn)行平臺(tái)Win32 + VC++2010):
#include
int main(void)
{
float fa=0.5,fb=1.25,fc=-1.25,fd=124.25;
unsigned int *pa=(unsigned int *)&fa;
unsigned int *pb=(unsigned int *)&fb;
unsigned int *pc=(unsigned int *)&fc;
unsigned int *pd=(unsigned int *)&fd;
printf("%.1f的內(nèi)存存儲(chǔ)形式:%X\n",fa,*pa);
printf("%.2f的內(nèi)存存儲(chǔ)形式:%X\n",fb,*pb);
printf("%.2f的內(nèi)存存儲(chǔ)形式:%X\n",fc,*pc);
printf("%.2f的內(nèi)存存儲(chǔ)形式:%X\n",fd,*pd) ;
return 0;
}
程序執(zhí)行結(jié)果:
0.5的內(nèi)存存儲(chǔ)形式:3F000000
1.25的內(nèi)存存儲(chǔ)形式:3FA00000
-1.25的內(nèi)存存儲(chǔ)形式:BFA00000
124.25的內(nèi)存存儲(chǔ)形式:42F88000
可見(jiàn)程序的執(zhí)行結(jié)果與上面的分析和計(jì)算結(jié)果是一致的。
在VC++2010的監(jiān)視窗口(圖6)和內(nèi)存窗口(圖7)觀察的結(jié)果如下圖所示。
清楚了Visual C++2010環(huán)境下單精度浮點(diǎn)數(shù)的具體存儲(chǔ)形式,需要進(jìn)一步探討浮點(diǎn)數(shù)的取值范圍和精度問(wèn)題。
7單精度浮點(diǎn)數(shù)的取值范圍和精度