☀️光天化日学C语言☀️(24)- 浮点数的存储 | 天才般的设计,反正我这么认为
一、前言
本文作者是从 2007 年开始学 C语言 的,不久又接触了C++,基本就是 C/C++ 技术栈写了 14 年的样子,不算精通,但也算差强人意。著有《夜深人静写算法》系列,且承诺会持续更新,直到所有算法都学完。主要专攻 高中 OI 、大学 ACM、 职场 LeetCode 的全领域算法。由于文章中采用 C/C++ 的语法,于是就有不少读者朋友反馈语言层面就被劝退了,更何况是算法。
于是,2021 年 06 月 12 日,《光天化日学C语言》 应运而生。这个系列文章主要服务于高中生、大学生以及职场上想入坑C语言的志同道合之人,希望能给祖国引入更多编程方面的人才,并且让自己的青春不留遗憾!
这一章的主要内容是浮点数的存储。
二、人物简介
- 第一位登场的就是今后会一直教我们C语言的老师 —— 光天。
- 第二位登场的则是今后会和大家一起学习C语言的没什么资质的小白程序猿 —— 化日。
三、浮点数简介
1、数学中的小数
- 数学中的小数分为整数部分和小数部分,它们由点号
.
分隔,我们将它称为 十进制表示。例如 、 、 、 等都是合法的小数,这是最常见的小数形式。 - 小数也可以采用 指数表示,例如 、 、 等。
2、C语言中的小数
- 在 C语言 中的小数,我们称为浮点数。
- 其中,十进制表示相同,而指数表示,则略有不同。
- 对于数学中的 。在C语言中的指数表示如下:
aEn 或者 aen
- 其中 为尾数部分,是一个十进制数; 为指数部分,是一个十进制整数; 、 是固定的字符,用于分割 尾数部分 和 指数部分。
数学 | C语言 |
---|---|
3、浮点数类型
- 常用浮点数有两种类型,分别是
float
和double
; 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)以指数形式输出浮点数时,输出结果为科学计数法。也就是说,尾数部分的取值为:
-
- 3)以上六个输出,对应的是表格中的六种输出方式,但是我们发现第一种输出方式中,并不是我们期望的结果,这是由于这个数超出了
float
能够表示的范围,从而产生了精度误差,而double
的范围更大一些,所以就能正确表示,所以平时编码过程中,如果对效率要求较高,对精度要求较低,可以采用float
;反之,对效率要求一般,但是对精度要求较高,则需要采用double
。
四、浮点数的存储
1、科学计数法
- C语言中,浮点数在内存中是以科学计数法进行存储的,科学计数法是一种指数表示,数学中常见的科学计数法是基于十进制的,例如 ;计算机中的科学计数法可以基于其它进制,例如 就是基于二进制的,它等价于 。
- 科学计数法的一般形式如下:
-
:代表要表示的浮点数;
:代表 的正负号,它的取值只能是 0 或 1:取值为 0 是正数,取值为 1 是负数;
:代表基数,或者说进制,它的取值大于等于 2;
:代表尾数,或者说精度,是 进制的小数,并且 ,这意味着,小数点前面只能有一位数字;
:代表指数,是一个整数,可正可负,并且为了直观一般采用 十进制 表示。
1)十进制的科学计数法
- 以 这个小数为例,根据初中学过的知识,想要把它转换成科学计数法,只要移动小数点的位置。如果小数点左移一位,则指数 加一;如果小数点右移一位,则指数 减一;
- 所以它在十进制下的科学计数法,根据上述公式,计算结果为:
-
- 其中 、 、 、 、 ;
- 这是我们数学中最常见的科学计数法。
2)二进制的科学计数法
- 同样以 这个小数为例,我们将它转换成二进制,按照两部分进行转换:整数部分和小数部分。
- 整数部分:整数部分等于 14,不断除 2 取余数,转换成 2 的幂的求和如下:
-
- 所以 14 的二进制表示为 。
- 小数部分:小数部分等于 0.375,不断乘 2 取整数部分的值,转换成 2 的幂的求和如下:
-
- 所以 0.375 的二进制表示为
- 将 整数部分 和 小数部分 相加,得到的就是它的二进制表示:
-
- 同样,我们参考十进制科学计数法的表示方式,通过移动小数点的位置,将它表示成二进制的科学计数法,对于这个数,我们需要将它的小数点左移三位。得到:
-
- 其中 、 、 、 、 ;
- 我们发现,为了表示成科学计数法,小数点的位置发生了浮动,这就是浮点数的由来。
2、浮点数存储概述
- 计算机中的浮点数表示都是采用二进制的。上面的科学计数法公式中,除了 确定是 2 以外,符号位 、尾数位 、指数位 都是未知数,都需要在内存中体现出来。还是以 为例,我们来看下它的几个关键数值的存储。
1)符号的存储
- 符号位的存储类似存储整型一样,单独分配出一个比特位来,用 0 表示正数,1 表示负数。对于 ,符号位的值是 0。
2)尾数的存储
- 根据科学计数法的定义,尾数部分的取值范围为 $$1 \le fraction \lt 2$$
- 这代表尾数的整数部分一定为 1,是一个恒定的值,这样就无需在内存中提现出来,可以将其直接截掉,只要把小数点后面的二进制数字放入内存中即可,这个设计可真是省(扣)啊。
- 对于
,就是把
110011
放入内存。我们将内存中存储的尾数命名为 ,真正的尾数命名为 ,则么它们之间的关系为: $$fraction = 1.f$$ - 这时候,我们就可以发现,如果 采用其它进制,那么尾数的整数部分就不是固定的,它有多种取值的可能,以十进制为例,尾数的整数部分可能是 之间的任何一个值,如此一来,尾数的整数部分就无法省略,必须在内存中表示出来。但是将 设置为 2,就可以节省掉一个比特位的内存,这也是采用二进制的优势。
3)指数的存储
- 指数是一个整数,并且有正负之分,不但需要存储它的值,还得能区分出正负号来。所以存储时需要考虑到这些。
- 那么它是参照补码的形式来存储的吗?
- 答案是否。
- 指数的存储方式遵循如下步骤:
- 1)由于
float
和double
分配给指数位的比特位不同,所以需要分情况讨论; - 2)假设分配给指数的位数为 个比特位,那么它能够表示的指数的个数就是 ;
- 3)考虑到指数有正负之分,并且我们希望正负指数的个数尽量平均,所以取一半, 表示负数, 表示正数。
- 4)但是,我们发现还有一个 0,需要表示,所以负数的表示范围将就一点,就少了一个数;
- 5)于是,如果原本的指数位 ,实际存储到内存的值就是:$$x + 2^{n-1} - 1$$
- 接下来,我们拿具体
float
和double
的实际位数来举例说明实际内存中的存储方式。
3、浮点数存储内存结构
- 浮点数的内存分布主要分成了三部分:符号位、指数位、尾数位。浮点数的类型确定后,每一部分的位数就是固定的。浮点数的类型,是指它是
float
还是double
。 - 对于
float
类型,内存分布如下:
- 对于
double
类型,内存分布如下:
- 1)符号位:只有两种取值:0 或 1,直接放入内存中;
- 2)指数位:将指数本身的值加上 转换成 二进制,放入内存中;
- 3)尾数位:将小数部分放入内存中;
浮点数类型 | 指数位数 | 指数范围 | 尾数位数 | 尾数范围 |
---|---|---|---|---|
float |
||||
double |
4、内存结构验证举例
- 以上文求得的 为例,我们将它转换成二进制,表示成科学计数法,如下:
-
- 其中 值 、符号位 、基数 、尾数 、指数 ;
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;
}
运算结果如下:
第一步,就是把上面那串二进制的 01串 直接拷贝下来,然后在前面加上0b
前缀,代表了 这个四字节的内存结构就是这样的;
第二步,分三个小步骤:
&value
代表取value
这个值的地址;
(float *)&value
代表将这个地址转换成float
类型;
*(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;
}
运算结果如下:
第一步,就是把上面那串二进制的 01串 直接拷贝下来,然后在前面加上0b
前缀,代表了 这个八字节的内存结构就是这样的;
第二步,分三个小步骤:
&value
代表取value
这个值的地址;
(double *)&value
代表将这个地址转换成double
类型;
*(double *)&value
代表将这个地址里的值按照double
类型解析得到一个double
数;
- 没错,运行结果也是:
14.375000
- 这块内容,如果你看的有点懵,没有关系,等我们学了指针的内容以后,再来回顾这块内容,你就会如茅塞一样顿开了!
- 你学废了吗?🤣
通过这一章,我们学会了:
浮点数的科学计数法和内存存储方式;
- 希望对你有帮助哦 ~ 祝大家早日成为 C 语言大神!
课后习题
📢博客主页:https://blog.csdn.net/WhereIsHeroFrom
📢欢迎各位 👍点赞 ⭐收藏 📝评论,如有错误请留言指正,非常感谢!
📢本文由 英雄哪里出来 原创,转载请注明出处,首发于 🙉 CSDN 🙉
作者的专栏:
👉C语言基础专栏《光天化日学C语言》
👉C语言基础配套试题详解《C语言入门100例》
👉算法进阶专栏《夜深人静写算法》
- 点赞
- 收藏
- 关注作者
评论(0)