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