2.6 浮点类型

浮点类型是用来表示那些带小数部分的数值的。实浮点类型一共有3种:float、double和long double。鉴于前面已经全面介绍了数据类型的基本概念,所以我们从double的常量入手。

2.6.1 double类型常量的代码书写规则

double类型常量有3种书写方法。

(1)十进制小数形式

规则:由0~9十个数字组成,带一个小数点。

例:3.14、3.、0.2、1.6

(2)十进制指数形式

规则如下。

■ 核心是一个字母E或e,其前后都必须有数。

■ E(或e)前面同(1)小数形式,但可以没有小数点。

■ E(或e)后面为一十进制整数(可带正负号)。

其中,E(或e)后面表示的是10的幂次(Exponent),整个double常量表示的数值为E前后数值的积。

例:2.6E7、.3e-5、4e+6、3.E-009

分别表示:

2.6×107、0.3×10-5、4×106、3.0×10-9

(3)十六进制指数形式(C99)

规则如下。

■ 核心是一个字母P或p,其前后都必须有数。

■ P(或p)前面为以0x(或0X(20))开头的十六进制小数或整数。

■ P(或p)后面可以跟一个正号或负号(也可以没有),再后是十进制数字序列。

其中,P(或p)后面表示的是2的幂次(Exponent),表示的数值为P(或p)前后数值的积。

例:0×12.EFP100、0xAB.CDP5

分别表示

2.6 浮点类型 - 图1

从表示形式上就可以看出,double常量和int常量一样,只有非负值。

double类型存储空间的问题同样不属于C语言的范畴,C语言并没有规定double类型的量应该占多少内存空间。目前,在通常的运行环境中,double类型的数据占64bits,而本书后面的讨论也都将以此为准。

2.6.2 浮点类型数据存储模型

double类型量的存储方式同样也不属于C语言的范畴。但在C语言中说明了浮点类型量的表示模型。为了更好地使用这种类型,这里对该模型做一简单的说明。以1.2345为例,由于计算机内只有二进制数,所以必须先把它换算成二进制形式:

2.6 浮点类型 - 图2

然后存储这个二进制数小数点后面的52位(最前面的1不用存,小数点后面的52位以后的部分也只能割爱了)。

把小数点后面的52位部分放到内存中用掉了52位,这部分叫尾数。8个字节共64位,64-52=12,所以还剩12位,其中的一位要记录这个数的正负,因而还有11位用来存放21中那个1,如图2-8所示,这部分叫指数部分。

2.6 浮点类型 - 图3

图2-8 编译器把代码中的double常量转换为二进制形式过程示意图

这段推演及其结果本身并没有多少实用价值,重要的是对它的哲学考量,具体如下。

(1)计算机的确是如本章开头所提到的那样,把这个世界抽象成了有限长度的有理数。

(2)正如int不对应数学中的整数一样,doule类型也不对应实数数轴上所有的点,而只是对应着数轴上的某些片段,且不是连续地对应,只是对应着数轴上某些片段上的离散点。换句话说,不是所有的实数都可以在代码中被精确地表示,double类型的量同样是个有限集,是一个定长的有理数,如图2-9所示。

2.6 浮点类型 - 图4

图2-9 double类型量只能表示数轴上部分区域内的一些离散点

(3)由于一般情况下,十进制小数不可能一定写成53位之内的二进制小数,所以double类型通常是一种不精确的近似值。不难理解的是,一个double类型的1.2345基本上可以断定并不精确地等于数学上的实数1.2345,或者也可以说一个double类型的1.2345很可能也等于一个double类型的1.2346(21)

(4)从前面的推演中可以清楚地看到,在源程序代码中,36与36.有着多么巨大的不同(22),说成是天壤之别也毫不过分,如图2-10所示。对此,编程者不可不察。

2.6 浮点类型 - 图5

图2-10 36与36.的区别

2.6.3 浮点类型的一些特性

从前面对浮点类型的存储模型中不难发现,浮点类型比整数类型要复杂的多。所以对于浮点类型,特性也更多。C语言没有规定几种浮点类型的存储空间,编译器对自己所采用的浮点类型的特征在float.h中有所规定(23)。但是C语言要求double类型的精度不得低于float类型,long double类型的精度不得低于double类型的精度。

下面的代码可择要输出编译器所采用的浮点类型的一些主要性质。

程序代码2-10

2.6 浮点类型 - 图6

屏幕输出如图2-11所示。

2.6 浮点类型 - 图7

图2-11 浮点类型的一些主要性质

从输出结果来看,long double类型值并没有成功地输出。这表明标准的这个要求并没有实现。这个问题从TC2.0的年代就一直存在(20世纪90年代前后)。而后面的内容不会涉及这种类型。

顺便说一下,float类型和long double类型常量的写法和double类型一样,但须加上后缀F或f和L或l。

这几种浮点类型表示数据的模式是类似的,不同的是各部分(指数部分和尾数部分)占的内存空间的大小不同。long double类型的精度在三者之中是最高的,但是C标准同样没有规定long double类型的长度。可以把float所能表示的数据视为double所能表示的数据的子集,同样double所表示的数据室long double所能表示数据的子集。

浮点类型比整数类型通常表示的数值的范围更大,但一般情况下不像后者那样是一种精确地表示。浮点类型在多数情况下都只能是一种近似表示,而这一点很容易被初学者所忽视。

在C99中增加了另外几种与_Complex、_Imaginary有关的浮点类型。这些类型将在后面适当章节介绍。

2.6.4 浮点类型的运算

对于浮点类型来说,%运算是不存在的。1.0%2.0是一种语法错误。

对于浮点类型来说,前面介绍的其他几种算术运算是有定义的。但和整数类型不同的是,运算结果只能要求是在相应的浮点类型可以表示的范围之内。存在运算结果是相应浮点类型所无法表示的可能,这时运算得到的值是一个近似值(这里还有个舍入方式的问题,这里就不讲了,读者了解有这么回事情就可以了)。

这些实浮点类型作为有限长度的有理数,主要用于近似的数值计算。在使用之前,了解编译器所支持的实浮点类型的特征是十分必要的(如表示范围等)。C语言要求编译器在float.h文件中提供这些类型所表示数据的特征和参数。

这些实浮点类型数据具有一些特殊值:无穷大(Infinity)和NaN(非数值:Not-a-Number)。例如,当一个double除以0.0或一个很小的数时,尽管发生了溢出,但有时并不发生执行时错误,而是会得到一个表示无穷大的特殊值。例如,3./0.的值为1.# INF00。

这些特殊的值通常也可以被输入、输出及参加计算,但这不是本书讨论的主要内容。在这里只是提醒一下,由于有这些特殊值参与运算,所以进行近似数值计算的结果有时实际上可能只是一种假象。

两个double数据运算时,可能因为结果太大而发生溢出(指数部分无法表示),同样可能以为结果太小而溢出(小数部分无法表示)。前者叫上溢,后者叫下溢(underflow)。

近似数值计算是十分复杂的问题,其本身就已经构成了一门学科,决不像某些人想象的那么成熟。使用浮点类型数据往往需要更加倍的谨慎。

此外需要了解的是,浮点类型数据的运算速度比整数类型的运算速度要慢。这点从浮点数据复杂的存储结构上就能体现。回想一下,小学中学习的计算“2345+34563”与“2.345 × 103+34563 × 104”在步骤上的差别,不难理解这一点。

练习

1.编程计算123/2和123./2.并输出,解释两者的差异。

2.编程计算123./0.0并输出,观察程序输出结果。

3.按照C语言源代码的格式要求写出下列各数并输出。

地球与太阳的距离:149,500,000(km)

地球的质量:5.9742×1024(kg)

水分子的直径1.925×10-9(m)

圆周率π的近似值

2.6.5 浮点类型的输出及其他

可以调用printf()函数输出double类型量,相应的转换格式如表2-1所示。

表2-1 double类型量的转换说明

2.6 浮点类型 - 图8

1 C99之前没有%F,只有%f。%F目前在多数编译器上还没有实现。

2 连续的64bits就可以满足最基本的语法要求,至于这64bits的解释是否得当是另一个问题。

3 C99新增加的格式,目前在多数编译器上还没有实现。

此外可以规定输出的宽度和精度,如%m.nf,其中m、n为两个整数,表示一共输出至少m位,小数点后为n位。但是当数据的实际宽度超过了指定的宽度时,则按照实际宽度输出。例如:

2.6 浮点类型 - 图9

其输出结果为:

2.6 浮点类型 - 图10

需要注意的,由于浮点类型多数情况下只是对数据近似表示,所以,可能会出现在屏幕上的输出与代码中写的浮点类型值有差别的情况。

另外在此顺便总结一下整数类型的输出格式,如表2-2所示。

表2-2 printf()函数的部分转换说明

2.6 浮点类型 - 图11

4 严格地说并不需要一定是int类型,只要是连续的4个字节就可以。

这里稍微解释一下视为正数的含义。假定机器内有如下32位二进制数:

1000 0000 0000 0000 0000 0000 0000 0000

如果这个数被视为正数,那么最高位的1表示1个231,否则,最高位为1通常是表示这是一个负数。当然,对最高位为0的数据不存在这种区别,无论如何都是一个正数。

所以对一个数据是否是正或负,printf()函数可以有自己的解释权而不是取决于原来的数据类型是什么。

对于long及long long类型的数据,分别需要在%及表示格式的字母之间加上l及ll。

特别要注意的是,转换说明与对应数据个数的一致与类型的匹配,否则可能输出无法预料的结果。

这里的几个转换说明(以%d为例)的更加一般的形式是%[0][m]d,其中m是一正整数,表示输出的宽度。但实际宽度若大于m值时按照实际宽度输出;0表示当输出宽度确实大于实际宽度时在前面补0。例如下面代码:

2.6 浮点类型 - 图12

输出的结果是:

2.6 浮点类型 - 图13