☀️光天化日学C语言☀️(24)- 浮点数的存储 | 天才般的设计,反正我这么认为

举报
英雄哪里出来 发表于 2021/08/17 07:07:34 2021/08/17
【摘要】 天才般的设计,反正我这么认为

一、前言

  本文作者是从 2007 年开始学 C语言 的,不久又接触了C++,基本就是 C/C++ 技术栈写了 14 年的样子,不算精通,但也算差强人意。著有《夜深人静写算法》系列,且承诺会持续更新,直到所有算法都学完。主要专攻 高中 OI 、大学 ACM、 职场 LeetCode 的全领域算法。由于文章中采用 C/C++ 的语法,于是就有不少读者朋友反馈语言层面就被劝退了,更何况是算法。
  于是,2021 年 06 月 12 日,《光天化日学C语言》 应运而生。这个系列文章主要服务于高中生、大学生以及职场上想入坑C语言的志同道合之人,希望能给祖国引入更多编程方面的人才,并且让自己的青春不留遗憾!
  这一章的主要内容是浮点数的存储。

二、人物简介

  • 第一位登场的就是今后会一直教我们C语言的老师 —— 光天。
  • 第二位登场的则是今后会和大家一起学习C语言的没什么资质的小白程序猿 —— 化日。

三、浮点数简介

1、数学中的小数

  • 数学中的小数分为整数部分和小数部分,它们由点号.分隔,我们将它称为 十进制表示。例如 0.0 0.0 1314.520 1314.520 1.234 -1.234 0.0001 0.0001 等都是合法的小数,这是最常见的小数形式。
  • 小数也可以采用 指数表示,例如 1.23. × 1 0 2 1.23.\times 10^2 0.0123 × 1 0 5 0.0123 \times 10^5 1.314 × 1 0 2 1.314 \times 10^{-2} 等。

2、C语言中的小数

  • 在 C语言 中的小数,我们称为浮点数。
  • 其中,十进制表示相同,而指数表示,则略有不同。
  • 对于数学中的 a × 1 0 n a \times 10^n 。在C语言中的指数表示如下:
aEn 或者 aen
  • 其中 a a 为尾数部分,是一个十进制数; n n 为指数部分,是一个十进制整数; E E e e 是固定的字符,用于分割 尾数部分 和 指数部分。
数学 C语言
1.5 1.5 1.5 E 1 1.5E1
1990 1990 1.99 e 3 1.99e3
0.054 -0.054 0.54 e 1 -0.54e-1

3、浮点数类型

  • 常用浮点数有两种类型,分别是floatdouble
  • float称为单精度浮点型,占 4 个字节;double称为双精度浮点型,占 8 个字节。

4、浮点数的输出

  • 我们可以用printf对浮点数进行格式化输出,如下表格所示:
控制符 浮点类型 表示形式
%f float 十进制表示
%e float 指数表示,输出结果中的 e小写
%E float 指数表示,输出结果中的 E大写
%lf double 十进制表示
%le double 指数表示,输出结果中的e小写
%lE double 指数表示,输出结果中的E大写
  • 来看一段代码加深理解:
#include <stdio.h>

int main() {
    float f = 520.1314f;
    double d = 520.1314;
    
    printf("%f\n", f);
    printf("%e\n", f);
    printf("%E\n", f);
    
    printf("%lf\n", d);
    printf("%le\n", d);
    printf("%lE\n", d);
    return 0;
}
  • 这段代码的输出如下:
520.131409
5.201314e+02
5.201314E+02
520.131400
5.201314e+02
5.201314E+02
  • 1)%f%lf默认保留六位小数,不足六位以 0 补齐,超过六位按四舍五入截断。
  • 2)以指数形式输出浮点数时,输出结果为科学计数法。也就是说,尾数部分的取值为:
  • 0 尾数 < 10 0 \le 尾数 \lt 10

  • 3)以上六个输出,对应的是表格中的六种输出方式,但是我们发现第一种输出方式中,并不是我们期望的结果,这是由于这个数超出了float能够表示的范围,从而产生了精度误差,而double的范围更大一些,所以就能正确表示,所以平时编码过程中,如果对效率要求较高,对精度要求较低,可以采用float;反之,对效率要求一般,但是对精度要求较高,则需要采用double

四、浮点数的存储

1、科学计数法

  • C语言中,浮点数在内存中是以科学计数法进行存储的,科学计数法是一种指数表示,数学中常见的科学计数法是基于十进制的,例如 5.2 × 1 0 11 5.2 × 10^{11} ;计算机中的科学计数法可以基于其它进制,例如 1.11 × 2 7 1.11 × 2^7 就是基于二进制的,它等价于 ( 11100000 ) 2 (11100000)_2
  • 科学计数法的一般形式如下:
  • v a l u e = ( 1 ) s i g n × f r a c t i o n × b a s e e x p o n e n t value = (-1)^{sign} \times fraction \times base^{exponent}

   v a l u e value :代表要表示的浮点数;
   s i g n sign :代表 v a l u e value 的正负号,它的取值只能是 0 或 1:取值为 0 是正数,取值为 1 是负数;
   b a s e base :代表基数,或者说进制,它的取值大于等于 2;
   f r a c t i o n fraction :代表尾数,或者说精度,是 b a s e base 进制的小数,并且 1 f r a c t i o n < b a s e 1 \le fraction \lt base ,这意味着,小数点前面只能有一位数字;
   e x p o n e n t exponent :代表指数,是一个整数,可正可负,并且为了直观一般采用 十进制 表示。

1)十进制的科学计数法

  • 14.375 14.375 这个小数为例,根据初中学过的知识,想要把它转换成科学计数法,只要移动小数点的位置。如果小数点左移一位,则指数 e x p o n e n t exponent 加一;如果小数点右移一位,则指数 e x p o n e n t exponent 减一;
  • 所以它在十进制下的科学计数法,根据上述公式,计算结果为:
  • ( 14.375 ) 10 = 1.4375 × 1 0 1 (14.375)_{10} = 1.4375 \times 10^1

  • 其中 v a l u e = 14.375 value = 14.375 s i g n = 0 sign = 0 b a s e = 10 base = 10 f r a c t i o n = 1.4375 fraction = 1.4375 e x p o n e n t = 1 exponent = 1
  • 这是我们数学中最常见的科学计数法。

2)二进制的科学计数法

  • 同样以 14.375 14.375 这个小数为例,我们将它转换成二进制,按照两部分进行转换:整数部分和小数部分。
  • 整数部分:整数部分等于 14,不断除 2 取余数,转换成 2 的幂的求和如下:
  • ( 14 ) 10 = 1 × 2 3 + 1 × 2 2 + 1 × 2 1 + 0 × 2 0 (14)_{10} = 1 \times 2^3 + 1 \times 2^2 + 1 \times 2^1 + 0 \times 2^0

  • 所以 14 的二进制表示为 ( 1110 ) 2 (1110)_2
  • 小数部分:小数部分等于 0.375,不断乘 2 取整数部分的值,转换成 2 的幂的求和如下:
  • ( 0.375 ) 10 = 0 × 2 1 + 1 × 2 2 + 1 × 2 3 (0.375)_{10} = 0 \times 2^{-1} + 1 \times 2^{-2} +1 \times 2^{-3}

  • 所以 0.375 的二进制表示为 ( 0.011 ) 2 (0.011)_2
  • 将 整数部分 和 小数部分 相加,得到的就是它的二进制表示:
  • ( 1110.011 ) 2 (1110.011)_2

  • 同样,我们参考十进制科学计数法的表示方式,通过移动小数点的位置,将它表示成二进制的科学计数法,对于这个数,我们需要将它的小数点左移三位。得到:
  • ( 1110.011 ) 2 = ( 1.110011 ) 2 × 2 3 (1110.011)_2 = (1.110011)_2 \times 2^3

  • 其中 v a l u e = 14.375 value = 14.375 s i g n = 0 sign = 0 b a s e = 2 base = 2 f r a c t i o n = ( 1.110011 ) 2 fraction = (1.110011)_2 e x p o n e n t = 3 exponent = 3
  • 我们发现,为了表示成科学计数法,小数点的位置发生了浮动,这就是浮点数的由来。

2、浮点数存储概述

  • 计算机中的浮点数表示都是采用二进制的。上面的科学计数法公式中,除了 b a s e base 确定是 2 以外,符号位 s i g n sign 、尾数位 f r a c t i o n fraction 、指数位 e x p o n e n t exponent 都是未知数,都需要在内存中体现出来。还是以 14.375 14.375 为例,我们来看下它的几个关键数值的存储。

1)符号的存储

  • 符号位的存储类似存储整型一样,单独分配出一个比特位来,用 0 表示正数,1 表示负数。对于 14.375 14.375 ,符号位的值是 0。

2)尾数的存储

  • 根据科学计数法的定义,尾数部分的取值范围为 $$1 \le fraction \lt 2$$
  • 这代表尾数的整数部分一定为 1,是一个恒定的值,这样就无需在内存中提现出来,可以将其直接截掉,只要把小数点后面的二进制数字放入内存中即可,这个设计可真是省(扣)啊。
  • 对于 ( 1.110011 ) 2 (1.110011)_2 ,就是把110011放入内存。我们将内存中存储的尾数命名为 f f ,真正的尾数命名为 f r a c t i o n fraction ,则么它们之间的关系为: $$fraction = 1.f$$
  • 这时候,我们就可以发现,如果 b a s e base 采用其它进制,那么尾数的整数部分就不是固定的,它有多种取值的可能,以十进制为例,尾数的整数部分可能是 1 9 1 \to 9 之间的任何一个值,如此一来,尾数的整数部分就无法省略,必须在内存中表示出来。但是将 b a s e base 设置为 2,就可以节省掉一个比特位的内存,这也是采用二进制的优势。

3)指数的存储

  • 指数是一个整数,并且有正负之分,不但需要存储它的值,还得能区分出正负号来。所以存储时需要考虑到这些。
  • 那么它是参照补码的形式来存储的吗?
  • 答案是否。
  • 指数的存储方式遵循如下步骤:
  • 1)由于floatdouble分配给指数位的比特位不同,所以需要分情况讨论;
  • 2)假设分配给指数的位数为 n n 个比特位,那么它能够表示的指数的个数就是 2 n 2^n
  • 3)考虑到指数有正负之分,并且我们希望正负指数的个数尽量平均,所以取一半, 2 n 1 2^{n-1} 表示负数, 2 n 1 2^{n-1} 表示正数。
  • 4)但是,我们发现还有一个 0,需要表示,所以负数的表示范围将就一点,就少了一个数;
  • 5)于是,如果原本的指数位 x x ,实际存储到内存的值就是:$$x + 2^{n-1} - 1$$
  • 接下来,我们拿具体floatdouble的实际位数来举例说明实际内存中的存储方式。

3、浮点数存储内存结构

  • 浮点数的内存分布主要分成了三部分:符号位、指数位、尾数位。浮点数的类型确定后,每一部分的位数就是固定的。浮点数的类型,是指它是float还是double
  • 对于float类型,内存分布如下:

  • 对于double类型,内存分布如下:


  • 1)符号位:只有两种取值:0 或 1,直接放入内存中;
  • 2)指数位:将指数本身的值加上 2 n 1 1 2^{n-1}-1 转换成 二进制,放入内存中;
  • 3)尾数位:将小数部分放入内存中;
浮点数类型 指数位数 指数范围 尾数位数 尾数范围
float 8 8 [ 2 7 + 1 , 2 7 ] [-2^7+1,2^7] 23 23 [ ( 0 ) 2 , ( 1...1 23 ) 2 ] [(0)_2, (\underbrace{1...1}_{23})_2]
double 11 11 [ 2 10 + 1 , 2 10 ] [-2^{10}+1,2^{10}] 52 52 [ ( 0 ) 2 , ( 1...1 52 ) 2 ] [(0)_2, (\underbrace{1...1}_{52})_2]

4、内存结构验证举例

  • 以上文求得的 14.375 14.375 为例,我们将它转换成二进制,表示成科学计数法,如下:
  • ( 1110.011 ) 2 = ( 1.110011 ) 2 × 2 3 (1110.011)_2 = (1.110011)_2 \times 2^3

  • 其中 值 v a l u e = 14.375 value = 14.375 、符号位 s i g n = 0 sign = 0 、基数 b a s e = 2 base = 2 、尾数 f r a c t i o n = ( 1.110011 ) 2 fraction = (1.110011)_2 、指数 e x p o n e n t = 3 exponent = 3

1)float 的内存验证

  • 为了方便阅读,我采用了颜色来表示数字,橙色代表符号位,蓝色代表指数位,红色代表尾数,绿色代表尾数补齐位;并且 八位一分隔,增强可视化。
  • 符号位的内存:0
  • 指数的内存(加上127后等于130,再转二进制):10000010
  • 尾数的内存(不足23位补零):1100110 00000000 00000000
  • 按顺序组织到一起后得到:01000001 01100110 00000000 00000000
#include <stdio.h>
int main() {
    int value = 0b01000001011001100000000000000000;  // (1)
    printf("%f\n",  *(float *)(&value) );            // (2)
    return 0;
}

运算结果如下:
   ( 1 ) (1) 第一步,就是把上面那串二进制的 01串 直接拷贝下来,然后在前面加上0b前缀,代表了 v a l u e value 这个四字节的内存结构就是这样的;
   ( 2 ) (2) 第二步,分三个小步骤:
    ( 2. a ) (2.a) &value代表取value这个值的地址;
    ( 2. b ) (2.b) (float *)&value代表将这个地址转换成float类型;
    ( 2. c ) (2.c) *(float *)&value代表将这个地址里的值按照float类型解析得到一个float数;

  • 运行结果为:
14.375000
  • (有关取地址和指针相关的内容,由于前面章节还没有涉及,如果读者看不懂,也没有关系,后面在讲解指针时会详细讲解这块内容,敬请期待)。

2)double 的内存验证

  • 为了方便阅读,我采用了颜色来表示数字,橙色代表符号位,蓝色代表指数位,红色代表尾数,绿色代表尾数补齐位;并且 八位一分隔,增强可视化。
  • 符号位的内存:0
  • 指数的内存(加上1023后等于1026,再转二进制):100 00000010
  • 尾数的内存(不足52位补零):1100 11000000 00000000 00000000 00000000 00000000 00000000
  • 按顺序组织到一起后得到:01000000 00101100 11000000 00000000 00000000 00000000 00000000 00000000
#include <stdio.h>
int main() {
    long long value = 0b0100000000101100110000000000000000000000000000000000000000000000;  // (1)
    printf("%lf\n",  *(double *)(&value) );                            // (2)
    return 0;
}

运算结果如下:
   ( 1 ) (1) 第一步,就是把上面那串二进制的 01串 直接拷贝下来,然后在前面加上0b前缀,代表了 v a l u e value 这个八字节的内存结构就是这样的;
   ( 2 ) (2) 第二步,分三个小步骤:
    ( 2. a ) (2.a) &value代表取value这个值的地址;
    ( 2. b ) (2.b) (double *)&value代表将这个地址转换成double类型;
    ( 2. c ) (2.c) *(double *)&value代表将这个地址里的值按照double类型解析得到一个double数;

  • 没错,运行结果也是:
14.375000
  • 这块内容,如果你看的有点懵,没有关系,等我们学了指针的内容以后,再来回顾这块内容,你就会如茅塞一样顿开了!
  • 你学废了吗?🤣

通过这一章,我们学会了:
  浮点数的科学计数法和内存存储方式;

  • 希望对你有帮助哦 ~ 祝大家早日成为 C 语言大神!

课后习题


📢博客主页:https://blog.csdn.net/WhereIsHeroFrom
📢欢迎各位 👍点赞 ⭐收藏 📝评论,如有错误请留言指正,非常感谢!
📢本文由 英雄哪里出来 原创,转载请注明出处,首发于 🙉 CSDN 🙉
作者的专栏:
  👉C语言基础专栏《光天化日学C语言》
  👉C语言基础配套试题详解《C语言入门100例》
  👉算法进阶专栏《夜深人静写算法》

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。