c语言保留小数点后n位_C语言(2)- 定点数和浮点数

(本文为原创,版权归作者所有)

变量的基本类型里包含了整数和小数,它们是如何由一组0和1来表示的呢?

在数学的世界里,实数可以涵盖一个数轴上所有的点,它应该可以表示我们在日常生活中碰到的大部分的数。实数分为有理数和无理数,有理数可以表示为两个整数相除,如p/q的形式,整数也是有理数;不能表示为p/q形式的数为无理数,比如圆周率π或者√2 。任何一个实数都可以用小数来表示,有的数可以表达为有限位数的小数,比如整数,小数部分为0,又比如1/4,小数表示为0.25;大部分实数则无法使用有限位的小数来表示,它们通常是无限循环或者不循环小数。但是在实际的数值计算中,由于物理上的限制,我们需要将这些实数近似为有限位数的数,近似数的位数则代表了这种近似的程度。比如π,如果表示为3.14,则它的有效位数为3位,如果表示为3.1415926,它的有效位数为8位,有效位数越大,近似程度也越高。

在计算机的世界里,一切的数都是二进制的。数的二进制表达常见的有两种方式:定点数和浮点数。

在不考虑符号的情况下,定点数类似于10进制中表示小数的方法,每一位代表2的n次幂,其中n是数位相对于小数点的位置。小数点左侧代表数的整数部分,小数点右侧代表数的纯小数部分,小数点左侧的第一位为0位。如下图所示定点数,它的数值为1*2^3+0*2^2+0*2^1+1*2^0+0*2^(-1)+1*2^(-2) = 9.25。

60cc201de03ad340ecb35e0c7ac02327.png

但是如何表示小数点的位置呢?显然我们没有办法用某一位来标记小数点的位置,一个32位的二进制小数的小数点位置有33种可能,那么至少需要5-6位才能表示,这显然是对内存的很大的浪费。稍后介绍的浮点数会采用其它的方式来标记浮点数中小数点的位置。事实上定点数的小数点位置是事先约定好的,位置是固定的,这也是称之为定点数的原因。整数就是一种最常见的定点数,它的小数点位固定在最后一位之后,也就是没有小数部分。如何约定定点数小数点的位置取决于语言或者程序的具体实现。

如果考虑到符号,那么事情就会变得复杂一些,因为我们需要用指定位置的1位数来表示正数或者负数。定点数利用最高位来表示符号,0代表正数,1代表负数。但并不是增加了1位就能解决问题,我们还要考虑硬件实现的复杂度。实际上,带符号的定点数可以有以下几种表示方式:

· 原码:正数和负数的区别只在符号位,数值部分是相同的

例如:

+7的原码:0000 0111
-7的原码:1000 0111
+0的原码:0000 0000
-0的原码:1000 0000

· 反码:正数的反码与原码相同,负数的反码,符号位为1,数值部分按位取反

例如:

+7的反码:0000 0111
-7的反码:1111 1000
+0的反码:0000 0000
-0的反码:1111 1111

· 补码:正数的补码与原码相同,负数的补码是负数的反码加1

例如:

+7的补码:0000 0111
-7的补码:1111 1001
+0的补码:0000 0000
-0的补码:0000 0000

可以看出,只有在补码的情况下,+0和-0的表示是一致的;此外使用补码进行加减运算时符号位可以和数值位统一处理,无符号数和有符号数的运算也是统一的,硬件可以不用关心符号位,只是按位进行加减运算即可,运算结果是有符号还是无符号数是由语言层面来负责解释的。有兴趣的读者可以试着用补码做一些运算,来验证以上的结论。

我们再从数学的角度探讨一下补码。考虑一个8位的二进制数,它表示数的范围是0-255,超出范围的数最终也只是保留在0-255之内,因此8位二进制数可以看作是一个数的模256表示。我们再来看一下-7的补码,根据定义,它是-7的反码加1,-7的反码是将+7按位取反,也可以写作255-7,因此-7的补码就是255-7+1,即-7+256。从模256的角度来看,a和a+256是等价的,-7和-7+256也是等价的,而 -7+256正是-7的补码,所以-7和-7的补码在数学上(模256)是等价的。当1111 1001(-7的补码)表示一个有符号数时,它被解释为-7,而当它表示一个无符号数时,它就被解释为-7+256=249,有符号数和无符号数就这样被统一了起来。

综上,无论是从硬件复杂度,还是从运算逻辑的简洁性来看,使用补码来表示有符号数是非常合理的选择。有符号定点数在计算机里通常是以补码的形式存储的。

定点数除了可以表示整数外也可以用来表示小数,但是必须事先约定小数点的位置,它的整数部分和小数部分的位数是固定的。受到字长的限制,定点数能表示的数的范围是有限的,表示数的数量也是有限的,比如32位定点数,它可以表示2^32那么多小数,但是考虑到小数的范围和密度,这实在是无法满足一般的计算要求。而将整数和小数的位数固定分配,也大大降低了有限的位资源的使用效率。采用浮点数可以解决定点数带来的部分问题。

浮点数是数的另一种表现形式,顾名思义,浮点数的小数点位置并不固定。我们讨论一下基于10进制的浮点数,比如:0.12345,1.2345,12.345,123.45,它们的浮点表示方式为:

·  0.12345 = 1.2345 x 10^-1
·  1.2345 = 1.2345 x 10^0
·  12.345 = 1.2345 x 10^1
·  123.45 = 1.2345 x 10^2

这其实就是我们数学中的科学计数法,其中,1.2345是有效数(Significand),又被称为尾数(Mantissa),10是基数(Base),而-1,0,1,2则是指数或者阶码(Exponent)。由此看出这几个数的浮点数表示除了阶码不同,其他部分是一致的,阶码实际上决定了浮点数小数点的位置。

我们来看一下浮点数在计算机中的二进制表示方式,这也是在IEEE 754标准里规定的格式,以32位单精度浮点数为例:

SEEEEEEE EMMMMMMM MMMMMMMM MMMMMMMM

其中S代表符号位,0表示最后的结果为正数,1表示最后的结果为负数。E代表阶码,是一个有符号的整数,共8位,采用的是移码编码(后面会介绍为什么使用移码)。M代表尾数,是一个无符号的定点纯小数,共23位,小数点前面还省略了一个1,省略的二进制1可以节省宝贵的1位数,从而增加浮点数的有效位,因此尾数的形式为1.MMM…(形如1.MMM…的二进制小数被称为规格化小数)。二进制小数的基数为2,不在浮点数里表示。所以上面的浮点数最后的数值为:

+1.M x 2^E或者-1.M x 2^E

这里的阶码E是8位,如果是无符号数可以表示的范围是1到254,其中0和255具有特殊的含义。为了使E可以取负值,采用移码,它实际代表的数值需要在无符号数的基础上减去一个固定的偏移量127,所以阶码E的实际范围为-126到+127。为什么要采用移码而不是直接使用补码呢?如果采用补码,考虑浮点数全0的情形,阶码的值为0,代表2^0=1,尾数全0代表的是1.0,按照上面的公式,全0的浮点数的值为1。我们竟然没有办法来表示0这个数(如果让阶码为-128,我们可以表示很小的浮点数,但依然无法表示0),这显然无法让人接受。于是IEEE 754将阶码E的0和255做特殊处理,其它的值经过移码可以表示-126到+127,具体规定如下(设浮点数数值为N,不考虑符号位):

· 若E=0,M=0,则N=0

· 若E=0,M!=0,则N=0.M x 2^-126 (0.M为非规格化的小数,可以表示更小的小数)

· 若1<=E<=254,则N=1.M x 2^(E-127) (1.M为规格化的小数)

· 若E=255,M!=0,则N=NaN(Not a Number,代表非数值)

· 若E=255,M=0,则N=∝ (无穷大)

因此,IEEE 754标准使浮点数的0有了精确表示,同时也明确的表示了无穷大。IEEE 754标准还规定了64位双精度浮点数的格式,它包含1位符号,11位阶码和52位尾数。

浮点数的精度完全取决于尾数即有效数有多少位,而阶码则决定了小数点的位置。32位单精度浮点数的尾数为23位,它的精度可以达到10进制的6到7位;64位双精度浮点数的尾数为52位,它的精度可以达到10进制的15到16位。需要注意的是,这里的精度不代表小数点后的位数,而是指有效位数,是从第一个非0的数开始计算的。所以浮点数不仅无法精确表达一个小数,也无法精确表达一个大的整数。仅就表示整数而言,尽管浮点数可以表示比定点整数更大的范围(例如32位浮点数可以表示2^127的大数),但它损失的是精确程度。

其实,无论是32位浮点数还是32位定点整数,它们能够表达的数的个数是一样的,都是2^32个。在不增加位数的情况下,我们无法期待数的表示方法可以即扩大数的表示范围,又能提高数的精确程度。定点整数可以精确地表示整数,浮点数可以灵活近似地表示小数,这就很好了。

浮点数的算数运算比定点数要复杂得多。比如做浮点数的加减法需要5步:

· 0操作数的检查:如果有0参与运算,就不必做下面的运算了,可以节省运算时间。

· 比较阶码大小并完成对阶:指数的加减法运算要在阶码相等的情况下进行,所以要先将两个数的阶码对齐,可以通过移动尾数的小数点来完成对阶。

· 尾数进行加或减的定点运算:这里的尾数是经过对阶调整后的尾数。

· 结果规格化:尾数要表示为1.M的形式,同时也要调整阶码。

· 舍入和溢出处理:尾数要进行舍入处理,阶码要进行溢出处理。以32位浮点数为例,阶码超过+127,则浮点数向上溢出,变为正无穷大;阶码小于-126,则浮点数向下溢出,变为0。

由此可见,浮点运算效率比定点运算要低得多。在有些计算机系统里会有专门的硬件(协处理器)来做浮点运算,如果没有相应的硬件,那么就只能靠软件来进行浮点运算了。因此在资源有限的嵌入式应用里,程序员都应该避免使用大量的浮点运算。

C语言(1-变量和类型:

蓝彼得:C语言(1)- 变量和类型​zhuanlan.zhihu.com
dff90d785de871517d8cdf8ae0c90c48.png

C语言(2-定点数和浮点数:

蓝彼得:C语言(2)- 定点数和浮点数​zhuanlan.zhihu.com
1660b70deddb6cda63b9d7f6074867de.png

C语言(3-运算符与表达式:

蓝彼得:C语言(3)- 运算符与表达式​zhuanlan.zhihu.com
1660b70deddb6cda63b9d7f6074867de.png

C语言(4-控制流:跳转、条件和循环:

蓝彼得:C语言(4)- 控制流:跳转、条件和循环​zhuanlan.zhihu.com
532785bf00b91dc0011e69115f893b54.png

C语言(5-内存模型与作用域:

蓝彼得:C语言(5)- 内存模型与作用域​zhuanlan.zhihu.com
5895dd155d9fcababb20de250fc00db9.png

C语言(6-函数调用和栈:

蓝彼得:C语言(6)- 函数调用和栈​zhuanlan.zhihu.com
1660b70deddb6cda63b9d7f6074867de.png

C语言(7-递归:

蓝彼得:C语言(7)- 递归​zhuanlan.zhihu.com
1660b70deddb6cda63b9d7f6074867de.png

版权声明:本文为weixin_29384861原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。