C语言学习(十一)小数在内存中是如何存储的?定点数与浮点数各自的优势在哪?规格化浮点数与非规格化浮点数又表示什么?

浮点数与定点数

小数在内存中以浮点数形式存储。浮点数并不是一种数值分类,他和整数、小数、实数等不是同一个层面的概念。浮点数是数字(或者说是数值)在内存中的一种存储格式,他是和定点数相对的。

C语言中规定使用定点数格式来存储short、int、long类型的整数,使用浮点数格式来存储float、double类型的小数。整数和小数在内存中存储的格式不一样。

我们通常认为浮点数和小数是等价的,并没有严格区别他们的概念,这也没有影响我们的学习,因为浮点数和小数是绑定在一起的,只有小数才使用浮点格式存储。

其实,整数和小数都可以使用定点格式来存储,也可以使用浮点格式来存储。而实际情况却是,C语言使用定点格式存储整数,浮点格式存储小数。这是在“数值范围”和“数值精度”两项重要指标之间追求平衡的结果,马上我们会具体分析。

浮点数和定点数中的“点”指的就是小数点!对于整数,可以认为小数点后面都是零,小数部分是否存在并不影响整个数字的值,所以干脆将小数部分省略,只保留整数部分。

定点数

所谓定点数,就是指小数点的位置是固定的,不会向前或向后移动。

假如我们用4个字节(32位)来存储无符号的定点数,并且约定,前16位表示整数部分,后16位表示小数部分,如下图:

这样的话,小数点就永远在这块内存的16位之后,整数部分和小数部分一目了然,不管什么时候,整数部分始终占用16位(不足16位就向前补0),小数部分也始终占用16位(不足16位就向后补0)。如,在内存中存储了 10101111 00110001 01011100 11000011,那么对应的小数就是 10101111 00110001 . 01011100 11000011。

精度

小数部分最后一位可能是精确数字,也可能是近似数字(由四舍五入、向零舍入等不同方式得到)。除此之外,剩余31位都是精确数字。从二进制的角度来看,这种定点格式的小数,最多有32位有效数字,能保证的是31位。整体精度位31~32位。

数值范围

将这块内存中所有位(Bit)都置为1,小数的值最大,为216-2-16,极其接近216,换算成十进制为65536。将内存中最后一位(第32位)置1,其他位都置0,小数的值最小,为2-16

这里所说的最小值是指最接近0的那个值

综述

用定点格式来存储小数,优点是精度高,因为所有位都用来存储有效数字了,缺点是取值范围太小,不能表示很大或很小的数字。

反面例子

在科学计算中,小数的取值范围很大,使用定点存储将不能满足需要。

例如,电子的质量为:

0.0000000000000000000000000009 克 = 9 × 10-28

太阳的质量为:

2000000000000000000000000000000000 克 = 2 × 1033

如果使用定点数,只能按照 = 前面的格式来存储,这将需要很大的一块内存,大到需要几十个字节。

更加好的方案其实是按照 = 后面的指数形式来存储,这样不但节省内存,还比较直观。

浮点数

这种以指数的形式来存储小数的解决方案就叫做浮点数。浮点数是对定点数的升级和优化,克服了定点数取值范围太小的缺点。

C语言标准规定,小数在内存中以科学计数法的形式来存储,具体为:

fflt = (-1)sign × mantissa × baseexponent

说明:

  • flt是要表示的小数
  • sign 用来表示 flt 的正负号,它的取值只能是 0 或 1:取值为 0 表示 flt 是正数,取值为 1 表示 flt 是负数。
  • base 是基数,或者说进制,它的取值大于等于 2(例如,2 表示二进制、10 表示十进制、16 表示十六进制……)。数学中常见的科学计数法是基于十进制的,例如 6.93 × 1013;计算机中的科学计数法可以基于其它进制,例如 1.001 × 27 就是基于二进制的,它等价于 1001 0000。
  • mantissa 为尾数,或者说精度,是 base 进制的小数,并且 1 ≤ mantissa < base,这意味着,小数点前面只能有一位数字;
  • exponent 为指数,是一个整数,可正可负,并且为了直观一般采用十进制表示。

下面我们以19.625为例演示如何将小数转换为浮点格式。

当base取值为10时,19.625的浮点形式为:

19.625 = 1.9625 x 101

当base取值为2时,将19.625转换成二进制为10011.101,用浮点形式来表示为:

19.625 = 10011.101 = 1.0011101 x 24

我们可以看出,当基数(进制)确定后,指数exponent实际上就成了小数点的移动位数:

  • exponent大于零,mantissa中的小数点右移exponent位即可还原小数值
  • exponent小于零,mantissa中的小数点左移exponent位即可还原小数值

换句话说,将小数转换成浮点数后,小数点的位置发生了浮动(移动),并且浮动的位数和方向由exponent决定,所以我们将这种表示小数的方式称为浮点数。

二进制形式的浮点数的存储

C语言标准并未规定base使用哪种进制,但是实际应用中,各种编译器都将base实现为二进制,这样不仅能贴近计算机硬件(任何数据在计算机底层都以二进制形式表示),还能减少转换次数。

我们接下来就聊聊如何将二进制形式的浮点数放入内存中。

原则上讲,上面的科学计数法公式中,符号sign、尾数mantissa、基数base和指数exponent都是不确定因素,都需要在内存中体现出来。现在基数已经确定是二进制了,就不用在内存中体现出来了,只需要存储符号sign、尾数mantissa、、指数exponent这三个不确定因素就可以了。

以19.625为例,将它转成二进制形式的浮点数格式:

19.625 = 1.0011101 x 24

此时符号位sign为0,尾数mantissa为1.0011101,指数为4。

符号的存储

符号的存储就和存储short、int等整数一样,单独分配一个位(Bit)来,用0表示正数,1表示负数。对于19.625,这一位的值是0。

尾数的存储

当采用二进制形式后,尾数部分的取值范围为1<=mantissa<2,这就意味着尾数部分的整数部分一定为1,是一个恒定的值,这样就不需要在内存中体现了,只需要把小数点后面的二进制数字放入内存即可。对于1.0011101,就是把0011101放入内存。

如果base采用其他进制,那么尾数的整数部分就不是固定的,他有多种取值可能。比如十进制,尾数的整数部分可能是1~9之间的任何一个值,这样尾数的整数部分就不能省略了,必须在内存中体现出来。而讲base设置为二进制就可以节省掉一个位(Bit)的内存,这也是采用二进制的一点优势。

指数的存储

指数是一个整数,并且有正负之分,不但需要存储他的值,还要能区分出正负号来。

区别于short、int、long的整数在内存中的存储,这里指数的存储并没有采用补码加符号位的形式,而是设计了另一套巧妙的方案,下面我会详细说。

为二进制浮点数分配内存

C语言中常用的浮点数类型为float和double。float始终占用4个字节,double始终占用8个字节。

这里演示了float和double的存储格式:

浮点数的内存被分成了三部分,分别用来存储符号sign、尾数mantissa和指数exponent,当浮点数类型确定后,每一部分的位数就是固定的。

符号sign可以不加修改直接放入内存中,尾数mantissa只需要将小数部分放入内存中,而最让我们疑惑的是指数exponent如何放入内存中,下面我们以float为例来说。

float指数部分占用8Bit,能表示0~255的值,取中间值127,指数在写入内存前先加上127,读取时再减去127,正数负数就显而易见了。19.625转换后的指数为4,4+127 = 131,131换算成二进制为1000 0011,这就是19.625的指数部分在float中的最终存储形式。

先确定内存中指数部分的取值范围,找到中间值,写入指数时加上这个中间值,读取指数时减去这个中间值,这样符号和值就都能确定下来了。

求中间值的公式(设中间值为median,指数部分占用的内存为n位):

median = 2n-1-1

对于float,中间值为28-1-1 = 127;对于double,中间值为211-1-1 = 1023。

后续文章中,我会采用这种命名:
mantissa表示真实的尾数,包括整数和小数部分;mant表示内存中存储的尾数,只有小数部分,省了整数部分。
exponent 表示真实的指数,exp 表示内存中存储的指数,exponent 和 exp 并不相等,exponent 加上中间数 median 才等于 exp。

精度问题

对于十进制小数,整数部分转换成二进制使用“展除法”(就是不断除以 2,直到余数为 0),一个有限位数的整数一定能转换成有限位数的二进制。但是小数部分就不一定了,小数部分转换成二进制使用“乘二取整法”(就是不断乘以 2,直到小数部分为 0),一个有限位数的小数并不一定能转换成有限位数的二进制,只有末位是 5 的小数才有可能转换成有限位数的二进制,其它的小数都不行。

float 和 double 的尾数部分是有限的,固然不能容纳无限的二进制;即使小数能够转换成有限的二进制,也有可能会超出尾数部分的长度,此时也不能容纳。这样就必须“四舍五入”,将多余的二进制“处理掉”,只保留有效长度的二进制,这就涉及到了精度的问题。也就是说,浮点数不一定能保存真实的小数,很有可能保存的是一个近似值。

对于float,尾数部分有23位,再加上一个隐含的整数1,一共是24位。最后一位可能是精确数字,也可能是近似数字(由四舍五、向零舍入等不同方式得到);除此之外,剩余23位都是精确数字。从二进制角度看,这种浮点格式的小数,最多有24位有效数字,但是能保证的只有23位;也就是说,整体精度为23~24位。如果转成十进制,224 = 16 777 216,一共8位;也就是说,最多有8位有效数字,但是能保证的是7位,所以得到整体精度为7~8位。

对于double,同理可得二进制形式的精度为 52~53 位,十进制形式的精度为 15~16 位。

IEEE 754 标准

浮点数的存储以及加减乘除运算是一个比较复杂的问题,很多小的处理器在硬件指令方面甚至不支持浮点运算,其他的则需要一个独立的协处理器来处理这种运算,只有最复杂的处理器才会在硬件指令集中支持浮点运算。省略浮点运算,可以将处理器的复杂度减半!如果硬件不支持浮点运算,那么只能通过软件来实现,代价就是需要容忍不良的性能。

PC 和智能手机上的处理器就是最复杂的处理器了,它们都能很好地支持浮点运算。

在六七十年代,计算机界对浮点数的处理比较混乱,各家厂商都有自己的一套规则,缺少统一的业界标准,这给数据交换、计算机协同工作带来了很大不便。

作为处理器行业的老大,Intel 早就意识到了这个问题,并打算一统浮点数的世界。Intel 在研发 8087 浮点数协处理器时,聘请到加州大学伯克利分校的 William Kahan 教授(最优秀的数值分析专家之一)以及他的两个伙伴,来为 8087 协处理器设计浮点数格式,他们的工作完成地如此出色,设计的浮点数格式具有足够的合理性和先进性,被 IEEE 组织采用为浮点数的业界标准,并于 1985 年正式发布,这就是 IEEE 754 标准,它等同于国际标准 ISO/IEC/IEEE 60559。

IEEE 是 Institute of Electrical and Electronics Engineers 的简写,中文意思是“电气和电子工程师协会”。

IEEE 754 简直是天才一般的设计,William Kahan 教授也因此获得了 1987 年的图灵奖。图灵奖是计算机界的“诺贝尔奖”。

目前,几乎所有的计算机都支持 IEEE 754 标准,大大改善了科学应用程序的可移植性,C语言编译器在实现浮点数时也采用了该标准。

不过,IEEE 754 标准的出现晚于C语言标准(最早的 ANSI C 标准于 1983 年发布),C语言标准并没有强制编译器采用 IEEE 754 格式,只是说要使用科学计数法的形式来表示浮点数,但是编译器在实现浮点数时,都采用了 IEEE 754 格式,这既符合C语言标准,又符合 IEEE 标准,何乐而不为。

特殊值

IEEE 754标准规定,当指数exp的所有位都为1时,不再作为“正常”的浮点数对待,而是作为特殊值处理:

  • 如果此时尾数mant的二进制位都为0,则表示无穷大:

    • 如果符号sign为1,则表示负无穷大
    • 如果符号sign为0,则表示正无穷大
  • 如果此时尾数mant的二进制位不全为0,则表示NaN(Not a Number),即这是一个无效的数字,或该数字未经初始化

非规格化浮点数

当指数exp的所有二进制位都为0时,情况也比较特殊。

对于“正常”的浮点数,尾数mant隐含的整数部分为1,并且在读取浮点数时,内存中的指数exp要减去中间值median才能还原真实的指数exponent,也即:

mantissa = 1.mant
exponent = exp - median

但是当指数exp的所有二进制位都为0时,一切都变了。尾数mant隐含的整数部分变成了0,并且用1减去中间值才能还原真实指数exponent,即:

mantissa = 0.mant
exponent = 1 - median

对于float,exponent = 1 - 127 = -126,指数exponent的值恒为 -126;对于double,exponent = 1 - 1023 = -1022,指数exponent的值恒为 -1022。

当指数exp的所有二进制位都是0时,我们将这样的浮点数称为“非规格化浮点数”;当指数exp所有二进制位既不全为0也不全为1时,我们称为“规格化浮点数”;当指数exp的所有二进制位都是1时,作为特殊值。也就是说,究竟是规格化浮点数,还是非规格化浮点数,还是特殊值,完全看指数exp。

对于非规格化浮点数,当尾数mant的所有二进制都为0时,整个浮点数的值就为0:

  • 如果符号sign为0,则表示+0
  • 如果符号sign为1,则表示-0

IEEE 754 为什么增加非规格化浮点数

我们以float类型来说明,先看下表:

^ 表示次方,例如 2^10 表示 2 的 10 次方

从表中我们不难发现,对于规格化浮点数,当尾数mant的所有位都为0、指数exp的最低位为1时,浮点数的绝对值最小,为1.0 x 2-126,也即2-126

对于一般的计算,这个值已经很小了,非常接近0值,但对于科学计算,他或许还不够小,非规格化浮点数就是来弥补这一点的。非规格化浮点数可以让最小值更小,更接近0值。

对于非规格化浮点数,当尾数的最低位为1时,浮点数的绝对值最小,为2-23 x 2-126 = 2-149,这个值比2-126小了23个数量级,更加接近0值。

规格化浮点数可以很平滑的过度到非规格化浮点数,他们之间不存在“断层”。

上表只演示了正数时的情况,负数与此类似。

舍入模式

浮点数的尾数部分mant所包含的二进制位有限,不能表示太长的数字,如果尾数部分过长,在放入内存时就必须将多余的位丢掉,取一个近似值。究竟如何来取这个近似值,IEEE 754列出了四种不同的舍入模式。

舍入到最接近的值

就是将结果舍入为最接近且可以表示的值,这是默认的舍入模式。这与我们平时所见的“四舍五入”类似,但是有一个细节不同。

对于最近舍入模式,IEEE 754 规定,当有两个最接近的可表示的值时首选“偶数”值。而对于四舍五入模式,当有两个最接近的可表示的值时选较大的值。
以十进制为例:

最近舍入模式:Round(0.5) = 0、Round(1.5) = 2、Round(2.5) = 2

四舍五入模式:Round(0.5) = 1、Round(1.5) = 2、Round(2.5) = 3

向+∞方向舍入(向上舍入)

会将结果朝正无穷大的方向舍入。标准库函数 ceil() 使用的就是这种舍入模式,例如,ceil(1.324) = 2,Ceil(-1.324) = -1。

向-∞方向舍入(向下舍入)

会将结果朝负无穷大的方向舍入。标准库函数 floor() 使用的就是这种舍入模式,例如,floor(1.324) = 1,floor(-1.324) = -2。

向0舍入(直接截断)

会将结果朝接近 0 的方向舍入,也就是将多余的位数直接丢掉。C语言中的类型转换使用的就是这种舍入模式,例如,(int)1.324 = 1,(int) -1.324 = -1。

总结

与定点数相比,浮点数在精度方面损失不小。但是在取值范围方面增大很多。牺牲精度,来换取取值范围,就是浮点数的整体思想。

IEEE 754 标准其实还规定了浮点数的加减乘除运算,这个我们之后再聊。

上篇问题解析

我们之前在用%f输出128.101时得到的是一个近似值,而不是一个精确值,这是因为128.101在转成浮点格式后,尾数部分过长,被丢掉了,精度丢失了一部分。

128.101转换成二进制为:

10000000.0001100111011011001000101101……(无限循环)

向左移动7位后为:

1.00000000001100111011011001000101101……

由此可见,尾数部分为:

000 0000 0001 1001 1101 1011 001000101101……

移除多出的二进制:

000 0000 0001 1001 1101 1011

使用printf输出时,还需要还原,还原后为:

10000000.0001100111011011

转换成十进制为128.1009979248046875,四舍五入的原则取6位小数,就是128.100998。

C语言学习(十一)小数在内存中是如何存储的?定点数与浮点数各自的优势在哪?规格化浮点数与非规格化浮点数又表示什么?相关推荐

  1. 计算机中小数如何储存,小数在内存中是如何存储的?

    小数在内存中是如何存储的? 文本关键字:小数.float.double.浮点数.精度 一.IEEE 754(二进制浮点数算术标准) 在学习进制转换时,我们了解到:我们经常使用的十进制数是转换为二进制进 ...

  2. 16 bit float 存储_小数在内存中是如何存储的,揭秘诺贝尔奖级别的设计(长篇神文)...

    小数在内存中是以浮点数的形式存储的.浮点数并不是一种数值分类,它和整数.小数.实数等不是一个层面的概念.浮点数是数字(或者说数值)在内存中的一种存储格式,它和定点数是相对的.C语言使用定点数格式来存储 ...

  3. 小数在内存中究竟是如何存储的(C语言代码详细讲解 2)

    小数在内存中是如何存储的,揭秘诺贝尔奖级别的设计 二进制形式的浮点数的存储   虽然C语言标准没有规定 base 使用哪种进制,但是在实际应用中,各种编译器都将 base 实现为二进制,这样不仅贴近计 ...

  4. 小数在内存中的存储表示

    整数在内存中的存储方式比较简单,我们来看看小数在内存中的存储方式.首先,要学会十进制小数与二进制小数之间的转换. (1)二进制小数转化为十进制小数 比如把二进制小数110.11转化为十进制小数,步骤如 ...

  5. c语言中小数在内存中的存储,c语言中小数在内存中的存储

    转至:https://blog..net/tqs_1220/article/details/73550899 首先看float和double在内存中是怎么存储的? float:符号位(1位)+指数位( ...

  6. C语言学习(三)内存初识、数据在内存中的保存形式、程序载入内存

    (一)内存与存储(硬盘)   首先要明确一点,内存不同于存储.内存中的数据存储在内存条中.而一般的存储,数据则保存在硬盘中.这里我只给出一些表象的区别,如果还想更深入的了解,请自行搜索,此篇所展示的重 ...

  7. C语言——深度剖析数据在内存中的存储

    大家好!我是保护小周ღ,本期为大家带来的是深度剖析数据在内存中的存储,不知道,大家学了这么久C语言,有没有想过一个问题,我们在程序设计中的数据是怎么在计算机中存储的?我们都知道 一个整型数据 int ...

  8. 【C语言】浮点型数据在内存中的存储方式

    目录 一. 前言 二. 问题的引出 三. 两类浮点型数据(float.double)在内存中的存储方式 3.1 两类浮点型数据的存储模型 3.1.1 浮点型数据数值读取的通用模型 3.1.2 floa ...

  9. C语言学习笔记Day3——持续更新中... ...

    上一篇文章C语言学习笔记Day2--持续更新中- - 八. 容器 1. 一维数组 1.1 什么是一维数组 当数组中每个元素都只带有一个下标(第一个元素的下标为0, 第二个元素的下标为1, 以此类推)时 ...

最新文章

  1. Spring Bean四种注入方式(Springboot环境)
  2. 如何在Swift中串联或合并数组?
  3. 【开源】SpringBootNetty聊天室V1.2.0升级版本介绍
  4. 【杂谈】GAN对人脸图像算法产生了哪些影响?
  5. CSS3 box-reflect 属性
  6. 开启应用的时候总是以管理员身份打开的解决方案
  7. 【C++深度剖析教程8】C++的操作符重载的概念
  8. matlab 判断两个矩阵有元素相等_如何使用MATLAB对Excel中的多参数进行计算?
  9. 牙齿间隙变大怎么办_牙齿缝隙大怎么办 5个方法解决牙齿缝隙大
  10. CSS — 内部样式几种常用写法(类、id)
  11. 实时计算在有赞的实践-效率提升之路
  12. 2.Linux性能诊断 --- 单机负载评估
  13. ssm学生助学贷款系统毕业设计(附源码、运行环境)
  14. Ubuntu完美安装QQ
  15. DHT11温湿度的电路连接和驱动(树莓派)
  16. 路由器的两种工作模式~~~
  17. 软件设计模式与体系结构实验——2.1-1(2)(抽象)工厂模式的应用
  18. uboot分析:uboot的启动过程分析
  19. 在线播放.html,网页嵌入式SWF视频播放器
  20. 常用激活函数:Sigmoid、Tanh、Relu、Leaky Relu、ELU、Mish、Swish等优缺点总结

热门文章

  1. 携程如何从海量数据中构建精准用户画像?
  2. 迷途的测试老鸟如何振翅高飞
  3. android kit ui,远近皆宜
  4. 如何解决undefined reference to `bblib_idft_burst_fxp'编译时函数未定义问题
  5. 使用Visdom可视化工具初体验
  6. 基于剪裁法的反射面设计
  7. react-native 集成友盟推送
  8. Project 修改每天的工作时间为12小时制
  9. 物联网通信协议大汇总
  10. 美术学习3310:绑定基础