浮点类型有效位数计算与应用分析

2019-06-09 10:36肖红德
软件导刊 2019年4期
关键词:存储单元

肖红德

摘 要:为弄清浮点类型数据在应用中存在“异常”现象的原因,研究了浮点类型数据在内存中的存储形式,得到与其在内存中存储规范一致的结果。分析浮点类型数据在内存中存储形式对应的理论区间并进行实验验证,得出浮点类型数据的精度(对于规范化浮点类型数据,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.

(责任编辑:何 丽)

猜你喜欢
存储单元
面向存储空间受限的分拣系统内多类型存储单元数量配置
一种28 nm工艺下抗单粒子翻转SRAM的12T存储单元设计
一种新型密集堆垛式仓储系统设计
一种FIFO的读写单元设计
一种成本更低的全新静态DRAM存储单元
MiR-125a-5p is Upregulated in Plasma of Residents from An Electronic Waste Recycling Site
OTP存储器存储单元内寄生电容对读取阈值的影响
极低电源电压和极低功耗的亚阈值SRAM存储单元设计
极低电源电压和极低功耗的亚阈值SRAM存储单元设计