☀️光天化日学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语言的没什么资质的小白程序猿 —— 化日。

三、浮点数简介

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×102 等。

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.54e1

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 

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 1fraction<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×21+1×22+1×23
  • 所以 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 1fraction<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 19 之间的任何一个值,如此一来,尾数的整数部分就无法省略,必须在内存中表示出来。但是将 b a s e base base 设置为 2,就可以节省掉一个比特位的内存,这也是采用二进制的优势。

3)指数的存储

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

3、浮点数存储内存结构

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

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


  • 1)符号位:只有两种取值:0 或 1,直接放入内存中;
  • 2)指数位:将指数本身的值加上 2 n − 1 − 1 2^{n-1}-1 2n11 转换成 二进制,放入内存中;
  • 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 
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 
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 语言大神!

课后习题

  • 【第06题】给定两个点的坐标 (x1, y1) 和 (x2, y2),求两点间的距离

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

你可能感兴趣的:(《光天化日学C语言》,C语言,C语言基础,浮点数存储,光天化日学C语言,教程)