探讨Visual C++2010环境下浮点型数据的存储形式

探讨Visual C++2010环境下浮点型数据的存储形式

探讨Visual C++2010环境下浮点型数据的存储形式

摘要:针对高校本科生课程《C语言程序设计》中有关浮点数数据类型的认识和使用中出现的问题,学生存在对浮点数的认知不够清晰,对Visual C++2010环境下有关浮点数的相关计算结果存在各种困惑。根据多年的教学经验,查阅相关书籍和IEEE754标准,论文分析Visual C++2010环境下浮点型数据的存储形式,阐述了有关浮点数相关几个重要知识点的理解。文中引入计算思维的指导思想,采用从现象到本质,从理论到实践来逐步解决问题。实践证明,该方法取得的了较好的学习效果,夯实了学生对基础知识的掌握和正确应用。

关键词:浮点数;IEEE754;存储形式;Visual C++2010环境;计算思维

中图分类号:TP311文献标识码:A

文章编号: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語言是最受欢迎、最重要的和最流行的编程语言之一。《C语言程序设计》课程是大部分大学本科理工类学生的一门必修课。C语言有丰富的数据类型,在介绍数据类型一章会介绍基本类型中的整型、字符型、浮点型。在这三种基本类型中浮点型是课程教学的重点和难点,根据多年的教学经验积累,笔者深入研究了Visual C++2010环境下浮点型数据的存储形式,梳理教学重点和难点,引入计算思维的思想,使得学生更容易理解浮点数的存储形式。

周以真教授认为:计算思维(Computational Thinking)是运用计算机科学的基础概念进行问题求解、系统设计以及人类行为理解等涵盖计算机科学之广度的一系列思维活动[1]。它包括了涵盖计算机科学之广度的一系列思维活动。当我们必须求解一个特定的问题时,首先会问:解决这个问题有多么困难?怎样才是最佳的解决方法?计算机科学根据坚实的理论基础来准确地回答这些问题。表述问题的难度就是工具的基本能力,必须考虑的因素包括机器的指令系统、资源约束和操作环境。

本文的思路是首先通过实践发现浮点数计算中存在的问题,为什么结果不准确?为什么会存在一些错误判断。要想弄清楚这些问题,需要清楚理论知识和计算机的操作环境,以及计算机计算能力的限制。

2 抛出问题

在使用浮点数的时候,我们发现一些问题:

(1)一个浮点数不能准确的输出,为什么?程序代码如下:

#include

int main()

{

float fa=56.982f;

printf(”fa=%f\n”,fa);

return 0;

}

这个程序的执行结果是:fa=56.981998,而不是fa=56.982。

(2)一个浮点数与一个极小的浮点数求和,结果不正确,为什么?程序代码如下:

#include

int main()

{

float fa=2.5f;

fa=fa+0.0000001f;

printf(”fa=%f\n”,fa);

return 0;

}

这个程序的执行结果是:fa=2.500000,而不是fa=2.5000001。 (3)数学上两个不相等的浮点数程序却认为是相等的,为什么?程序代码如下:

#include

int main()

{

float fa=1.00000001f,fb=1.00000002f;

if(fa==fb)

printf(”两个数相等!\n”);

return 0;

}

这个程序的执行结果是:“两个数相等!”,显然是错误的。

笔者所给的三个程序的运行环境是Win7+Visual C++2010,因此带着这几个疑问,有必要深入研究一下在这样的编译和运行环境下浮点数的存储形式。

3 IEEE754标准

浮点数是指小数点位置根据需要可以浮动的数据,浮点数的一般表示形式为:N=RE×D,其中N为浮点数,R称为基数,E称为阶码,D称为尾数。如图1所示。

R为一常数,与尾数的基数相同,一般为2、8、或16,在一台计算机中,所有数据的R都是相同的,不需要在每个数据中表示出来。任意一个二进制浮点数N可以表示为:N=2E×D。

在浮点数表示中,即使数据字长相同时,不同的计算机可能选用不同的格式、不同的阶码与尾数位数及其编码,从而导致不同计算机之前浮点机器数差异很大,不利于软件移植,为此,美国电气电子工程师协会于1985年提出了浮点机器数IEEE754标准,并得到广泛应用[2]。在软件中IEEE 754以浮点库的形式实现,在硬件(如许多CPU和FPU)中的指令中实现。 第一个实现IEEE 754-1985草案的集成电路是Intel 8087。

当今流行的计算机几乎都采用IEEE754浮点数标准,在这个标准中,每个浮点数均由数符S、阶码E、尾数M三部分组成。用三元组{S,E,M}表示一个数N,如图2所示。

IEEE754标准规定了四种浮点数的表示方式:单精确度(32位)、双精确度(64位)、延伸单精确度(43比特以上,很少使用)与延伸双精确度(79比特以上,通常以80比特实做)。C语言中float和double浮点型分别对应的是单精度和双精度浮点数,下面以单精度浮点数为例介绍浮点数的存储形式,如图3所示:单精度浮点数(32位):N共32位,其中S占1位,E占8位,M占23位。

单精度浮点数根据阶码和尾数的取值情况分为:0,非规格化数,规格化数,无穷和NaN(Not a Number,非数)。如表1所示。

浮点数的尾数采用补码形式存储,阶码采用移码形式存储。下面需要清楚什么是补码和移码。

4 原码、反码、补码、移码的概念

要想了解浮点数在计算机中的存储学生务必清楚原码、反码、补码、移碼相关概念。原码、补码,反码是把符号和数值一起编码的表示方法,是机器数的表示形式。

(1)原码:是用“符号码+二进制绝对值”表示的机器码。符号码用0表示正,用1表示负。

(2)反码:引入反码是为了求负数的补码。正数的反码与原码相同,负数的反码除符号位不变外,其余各位取反。

(3)补码:补码的引入是为了在计算机中把减法运算转换为加法运算。使减法运算变得简单,在计算机中容易实现。正数的补码与原码相同,负数的补码在反码的末尾加1。

(4)移码:又称为增码或偏码,在计算机中主要用于表示浮点数的阶码,而阶码是整数,因此阶码仅需要定点证书编码法。移码实质是在真值X基础上加一个固定正整数(称为偏置值),把真值映射到一个正数域,相当于在数轴上将真值X向正方向平移一段距离,这也是该编码命名为“移码”的来由。对于二进制纯整数,移码与真值得映射式为:[X]移=偏置值+X。偏置值选取的原则是使真值X中绝对值最大的负数对应的编码量值为0,因此偏置值一般取2n。当偏置值取2n时,移码与真值得映射式为:[X]移=2n+X (-2n <=X<=(2n-1))。

纯整数偏置值2n移码与补码之间的关系为:同一纯整数真值的偏置值2n移码与补码,它们的有效数值位相同,符号位互反。

清楚补码和移码编码之后,对于多个字节的浮点数,在存储器中应该怎么存储呢?

5 计算机存储的大端法和小端法

对于多字节的程序对象,必须建立两个规则:这个对象的地址是什么,以及在存储器中如何排列这些字节。在几乎所有的机器上,多字节对象都被存储为连续的字节序列,对象的地址为所使用在字节最小的地址。例如:假设在VC++2010中编译的C程序中一个int的变量x的地址是a,那么x的4个字节将分别被存储在存储器地址为:a、a+1、a+2、a+3的四个存储单元。

某些机器选择在存储器中按照从最低有效字节到最高有效字节的顺序存储对象,而另一些机器则按照从最高有效字节到最低有效字节的顺序存储。最低有效字节在前面的方式,称为“小端法(little endian)”。最高有效字节在最前面的方式,称为“大端法(big endian)”。大多数Intel兼容机都采用“小端法”的规则。大多数IBM和Sun Microsystems的机器都采用“大端法”的规则。IBM和Sun制造的个人计算机使用的是Intel兼容的处理器,因此使用的就是“小端法”。许多比较新的微处理器使用“双端法(bi-endian)”,也就是可以把它们配置成作为大端或者小端的机器运行[3]。

示例:变量x类型为int,位于地址a,int型数据在VC++2010中占32字节,假如它的十六进制为0x01234567。采用“大端法”(图4)和“小端法”(图5)的存储顺序分别如图4和图5所示。

可以通过下面的程序确认机器所采用的存储方式,代码编译运行平台Win32 + VC++2010,机器为联想ThinkPad T430,CPU型号是Intel酷睿i52520M。 #include

int main()

{

int x=0x01234567;

char *px=(char *)&x;

if(*px==0x01) //判断高字节0x01是否保存在低地址

printf(”大端法\n”);

else

printf(”小端法\n”);

return 0;

}

程序执行结果:“小端法”。

清楚了当前计算机采用的是“小端法”之后,需要通过程序来验证浮点数的具体存储形式。

6 Visual C++2010环境下单精度浮点数的具体存储形式

下面以具体的数为例,在编译环境下验证浮点数的存储形式。

(1)十进制0.5

第一步:把十进制0.5转换为二进制结果为(0.5)10=(0.1)2。

第二步:把(0.1)2规格化为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。

第四步:求解机器码(实际存储形式):[0.5]10= [0011 1111 0000 0000 0000 0000 0000 0000]2。

第五步:用十六进制表示机器数:3F000000。

(2)十进制1.25

第一步:把十进制1.25转换为二进制结果为(1.25)10=(1.01)2。

第二步:把(1.01)2规格化为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。

第四步:求解机器码(实际存储形式):[1.25]10= [0011 1111 1010 0000 0000 0000 0000 0000]2。

第五步:用十六进制表示机器数:3FA00000。

(3)十进制-1.25

第一步:把十进制-1.25转换为二进制结果为(-1.25)10=(-1.01)2。

第二步:把(-1.01)2规格化为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。

第四步:求解机器码(实际存储形式):[-1.25]10= [1011 1111 1010 0000 0000 0000 0000 0000]2。

第五步:用十六进制表示机器数:BFA00000。

(4)十进制124.25

第一步:把十进制124.25转换为二进制结果为(124.25)10=( 1111100.01)2

第二步:把( 1111100.01)2规格化为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。

第四步:求解机器码(实际存储形式):[124.25]10= [0100 0010 1111 1000 1000 0000 0000 0000]2。

第五步:用十六进制表示机器数:42F88000。

程序验证(代码编译运行平台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的内存存储形式:%X\n”,fa,*pa);

printf(”%.2f的内存存储形式:%X\n”,fb,*pb);

printf(”%.2f的内存存储形式:%X\n”,fc,*pc);

printf(”%.2f的内存存储形式:%X\n”,fd,*pd) ;

return 0;

}

程序执行结果:

0.5的内存存储形式:3F000000

1.25的内存存储形式:3FA00000

-1.25的内存存储形式:BFA00000

124.25的内存存储形式:42F88000

可见程序的执行结果与上面的分析和计算结果是一致的。

在VC++2010的监视窗口(图6)和内存窗口(图7)观察的结果如下图所示。

清楚了Visual C++2010环境下单精度浮点数的具体存储形式,需要进一步探讨浮点数的取值范围和精度问题。

7单精度浮点数的取值范围和精度 以32位单精度浮点数为例,阶码E由8位表示,取值范围为0-255,去除0和255这两种特殊情况,那么指数e的取值范围就是1-127=-126到254-127=127。

7.1 取值范围

(1)最大正数

最大正数值的符号位S=0,阶码E=254,指数e=254-127=127,尾数M=111 1111 1111 1111 1111 1111,其机器码为:0 11111110 111 1111 1111 1111 1111 1111,对应的十六进制数:0x7f7fffff。

最大正数:

PosMax=(?1)S×1.M×2e=(-1)0×1. 111 1111 1111 1111 1111 1111×2127≈3.402823e+38

(2)最小正数

最小正数符号位S=0,阶码E=1,指数e=1-127=-126,尾数M=0,其机器码为0 00000001 000 0000 0000 0000 0000 0000,对应的十六进制数:0x800000。

最小正数:PosMin=(?1)S×1.M×2e=(-1)0×(1.0)×2?126≈1.175494e?38。

(3)最大负数

最大负数符号位S=1,阶码E=1,指数e=1-127=-126,尾数M=0,机器码与最小正数的符号位相反,其他均相同,为:1 00000001 000 0000 0000 0000 0000 0000,对应的十六进制数:0x80800000。

最大负数: NegMax=(?1)S×1.M×2e=(?1)1×(1.0)×2?126≈?1.175494e?38。

(4)最小负数

最小负数符号位S=1,阶码E=254,指数e=254-127=127,尾数M=111 1111 1111 1111 1111 1111,其机器码为:1 11111110 111 1111 1111 1111 1111 1111,對应的十六进制数:0xFF7FFFFF。

最小负数:

NegMin=(?1)S×1.M×2e=(-1)1+(1.11111111111111111111111)×2127=?3.402823e+38。

验证程序如下:

#include

int main(void)

{

unsigned int fmax=0x7f7fffff;

unsigned int fmin=0x800000;

unsigned int nmax=0x80800000;

unsigned int nmin=0xFF7FFFFF;

float *pfmax=(float *)&fmax;

float *pfmin=(float *)&fmin;

float *pnmax=(float *)&nmax;

float *pnmin=(float *)&nmin;

printf(”最大正数是:%e\n”,*pfmax);

printf(”最小正数是:%e\n”,*pfmin);

printf(”最大负数是:%e\n”,*pnmax);

printf(”最小负数是:%e\n”,*pnmin);

return 0;

}

程序执行结果:

最大正数是:3.402823e+038

最小正数是:1.175494e-038

最大负数是:-1.175494e-038

最小负数是:-3.402823e+038

在VC++2010的监视窗口(图8)和内存窗口(图9)观察结果如下图所示。

由以上分析确认32位单精度浮点数的取值范围为:

[-3.402823e+038, -1.175494e-038],0,[ 1.175494e-038, 3.402823e+038]。

7.2 精度

计算机中的浮点数为什么有精度问题?计算机中的浮点数对应于数学中的实数。以32位浮点数为例,32位最多可以表示232个数,但从数学上说区间[0,1]中的小数就有无穷多个,所以计算机不可能描述所有实数,必然会有一些近似值,因此就会出现精度损失。

关于精度问题常见三种说法:

(1)32位单精度浮点数尾数域有23位,如果用二进制表示精度则有23位。23位能表示的最大数是223-1=8388607,所以尾数部分用十进制表示最大是8388607,尾数数值超过这个值,float将无法精确表示,所以float最多能表示小数点后7位,但绝对能保证的为6位,也即float的十进制的精度为6~7位;

(2)单精度数的尾数用23位存储,加上默认的小数点前的1位1,2 (23+1) = 16777216。因为 107 < 16777216 < 108,所以说单精度浮点数的十进制有效位数是7~8位;

(3)float 尾数占23位,带有一个固定隐含位,float有24个二进制的有效位数。

log10(224) = 7.2247198959355486851297334733878,说明float有7个十进制的有效位数。

8 结语

本文采用计算思维的指导思想,从5个方面对浮点数的存储形式进行了阐述、分析和验证。从理论到实践,从现象到本质,讲解有关浮点数相关的重点知识,使得学生能够更深刻地认识浮点数,理解计算机在解决问题时候的能力限制,使得学生在实际应用中能够更正确地使用浮点数。

参考文献:

[1] Jeannette M W. Computational thinking[J]. Communications of ACM, 2006, 49(3): 33-35

[2] 刘超,周新,郑燚,等. 计算机组成原理[M]. 北京: 清华大学出版社,2019:52-53.

[3] 布莱恩特,奥哈拉伦,深入理解计算机系统[M]. 龚奕利,等译.北京:机械工业出版社,2010:26-27.

【通联编辑:王力】

本文来自投稿,不代表多笔记立场,如若转载,请注明出处:https://www.duobiji.com/139429.html

版权归原作者所有,如有侵权、虚假错误信息或任何问题,请及时联系我们,我们将在第一时间删除或更正。

W 导出为探讨Visual C++2010环境下浮点型数据的存储形式.doc文档