《光天化日学C语言》|??光天化日学C语言??(24)- 浮点数的存储 | 天才般的设计,反正我这么认为
饭不食,水不饮,题必须刷
C语言免费动漫教程,和我一起打卡! 《光天化日学C语言》
LeetCode 太难?先看简单题! 《C语言入门100例》
数据结构难?不存在的! 《数据结构入门》
LeetCode 太简单?算法学起来! 《夜深人静写算法》
文章目录
- 一、前言
- 二、人物简介
- 三、浮点数简介
-
- 1、数学中的小数
- 2、C语言中的小数
- 3、浮点数类型
- 4、浮点数的输出
- 四、浮点数的存储
-
- 1、科学计数法
-
- 1)十进制的科学计数法
- 2)二进制的科学计数法
- 2、浮点数存储概述
-
- 1)符号的存储
- 2)尾数的存储
- 3)指数的存储
- 3、浮点数存储内存结构
- 4、内存结构验证举例
-
- 1)float 的内存验证
- 2)double 的内存验证
- 课后习题
一、前言
??本文作者是从 2007 年开始学 C语言 的,不久又接触了C++,基本就是 C/C++ 技术栈写了 14 年的样子,不算精通,但也算差强人意。著有《夜深人静写算法》系列,且承诺会持续更新,直到所有算法都学完。主要专攻 高中 OI 、大学 ACM、 职场 LeetCode 的全领域算法。由于文章中采用 C/C++ 的语法,于是就有不少读者朋友反馈语言层面就被劝退了,更何况是算法。二、人物简介
??于是,2021 年 06 月 12 日,《光天化日学C语言》 应运而生。这个系列文章主要服务于高中生、大学生以及职场上想入坑C语言的志同道合之人,希望能给祖国引入更多编程方面的人才,并且让自己的青春不留遗憾!
??这一章的主要内容是浮点数的存储。
- 第一位登场的就是今后会一直教我们C语言的老师 —— 光天。
- 第二位登场的则是今后会和大家一起学习C语言的没什么资质的小白程序猿 —— 化日。
- 数学中的小数分为整数部分和小数部分,它们由点号
.
分隔,我们将它称为 十进制表示。例如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 等。
- 在 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 |
- 常用浮点数有两种类型,分别是
float
和double
; float
称为单精度浮点型,占 4 个字节;double
称为双精度浮点型,占 8 个字节。
- 我们可以用
printf
对浮点数进行格式化输出,如下表格所示:
控制符 | 浮点类型 | 表示形式 |
---|---|---|
%f |
float |
十进制表示 |
%e |
float |
指数表示,输出结果中的 e 小写 |
%E |
float |
指数表示,输出结果中的 E 大写 |
%lf |
double |
十进制表示 |
%le |
double |
指数表示,输出结果中的e 小写 |
%lE |
double |
指数表示,输出结果中的E 大写 |
- 来看一段代码加深理解:
#include 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
。
- 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 = https://www.it610.com/article/(-1)^{sign} /times fraction /times base^{exponent} value=(?1)sign×fraction×baseexponent
?? v a l u e value value:代表要表示的浮点数;1)十进制的科学计数法
?? 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?? e x p o n e n t exponent exponent:代表指数,是一个整数,可正可负,并且为了直观一般采用 十进制 表示。
- 以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 = https://www.it610.com/article/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;
- 这是我们数学中最常见的科学计数法。
- 同样以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 = https://www.it610.com/article/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 为例,我们来看下它的几个关键数值的存储。
- 符号位的存储类似存储整型一样,单独分配出一个比特位来,用 0 表示正数,1 表示负数。对于14.375 14.375 14.375,符号位的值是 0。
- 根据科学计数法的定义,尾数部分的取值范围为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,就可以节省掉一个比特位的内存,这也是采用二进制的优势。
- 指数是一个整数,并且有正负之分,不但需要存储它的值,还得能区分出正负号来。所以存储时需要考虑到这些。
- 那么它是参照补码的形式来存储的吗?
- 答案是否。
- 指数的存储方式遵循如下步骤:
- 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
的实际位数来举例说明实际内存中的存储方式。
- 浮点数的内存分布主要分成了三部分:符号位、指数位、尾数位。浮点数的类型确定后,每一部分的位数就是固定的。浮点数的类型,是指它是
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?,(231...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?,(521...1??)2?] |
- 以上文求得的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 = https://www.it610.com/article/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;
- 为了方便阅读,我采用了颜色来表示数字,橙色代表符号位,蓝色代表指数位,红色代表尾数,绿色代表尾数补齐位;并且 八位一分隔,增强可视化。
- 符号位的内存:0
- 指数的内存(加上127后等于130,再转二进制):10000010
- 尾数的内存(不足23位补零):1100110 00000000 00000000
- 按顺序组织到一起后得到:01000001 01100110 00000000 00000000
#include
int main() {int value = https://www.it610.com/article/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
- (有关取地址和指针相关的内容,由于前面章节还没有涉及,如果读者看不懂,也没有关系,后面在讲解指针时会详细讲解这块内容,敬请期待)。
- 为了方便阅读,我采用了颜色来表示数字,橙色代表符号位,蓝色代表指数位,红色代表尾数,绿色代表尾数补齐位;并且 八位一分隔,增强可视化。
- 符号位的内存:0
- 指数的内存(加上1023后等于1026,再转二进制):100 00000010
- 尾数的内存(不足52位补零):1100 11000000 00000000 00000000 00000000 00000000 00000000
- 按顺序组织到一起后得到:01000000 00101100 11000000 00000000 00000000 00000000 00000000 00000000
#include
int main() {long long value = https://www.it610.com/article/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 语言大神!
- 【第06题】给定两个点的坐标 (x1, y1) 和 (x2, y2),求两点间的距离
博客主页:https://blog.csdn.net/WhereIsHeroFrom
欢迎各位 点赞 ?收藏 评论,如有错误请留言指正,非常感谢!
本文由 英雄哪里出来 原创,转载请注明出处,首发于 CSDN
作者的专栏:
??C语言基础专栏《光天化日学C语言》
??C语言基础配套试题详解《C语言入门100例》
??算法进阶专栏《夜深人静写算法》
推荐阅读
- 慢慢的美丽
- 《真与假的困惑》???|《真与假的困惑》??? ——致良知是一种伟大的力量
- 《跨界歌手》:亲情永远比爱情更有泪点
- 诗歌:|诗歌: 《让我们举起世界杯,干了!》
- 期刊|期刊 | 国内核心期刊之(北大核心)
- 《魔法科高中的劣等生》第26卷(Invasion篇)发售
- 人间词话的智慧
- 《一代诗人》37期,生活,江南j,拨动心潭的一泓秋水
- 广角叙述|广角叙述 展众生群像——试析鲁迅《示众》的展示艺术
- 书评——《小行星》