距离上次更新本文已经过去了 351 天,文章部分内容可能已经过时,请注意甄别

[TOC]

引言

本文首发于 ❄️慕雪的寒舍

markdown 语法小知识点

写在前面,markdown 语法的小知识点

如何实现文字变红且加上了底色?如 hello world

markdown 语法如下即可!

plaintext
1
`hello world`

以及页内跳转

plaintext
1
2
<span id="jump">这一句话没啥用</span>
[回到开头](#jump)

正题

之前学习完了整形、字符类型在内存中的存储,今天让我们来看看 float 类型!

整数类型👉【int】

字符类型👉【char】

常见的浮点数

c
1
2
3.14159
1E10

浮点数家族包括 floatdoublelong double 类型。

而浮点数表示的范围是在头文件 <float.h> 里面定义的。

需要了解的是

如果你打出 3.14,编译器默认是 double 类型的。若想让他为 float 类型,则要在前面加 f

1E10 是科学计数法,代表 1.0×10^10

代码引例

先来看看下面这串代码

c
1
2
3
4
5
6
7
8
9
10
11
int main()
{
int n = 9;
float* pfloat = (float*)&n;
printf("n的值为:%d\n", n);
printf("*pfloat的值为:%f\n", *pfloat);
*pfloat = 9.0;
printf("num的值为:%d\n", n);
printf("*pfloat的值为:%f\n", *pfloat);
return 0;
}

运行的结果如下

image-20220110135611980

指针 pfloat 保留的是强制转换为 float 类型的 int 变量 n

准确来说,是将 int 指针类型强制转换为了 float 指针类型

那打印的结果不应该是 9 吗?为什么是 0.000000 呢?

再来看看后面的代码,我们让 * pfloat=9.0,用 % d 打印的时候,却打印出了一串不知道怎么来的很大的数字。这又是为什么呢?

一个涉及到的小知识点

  • 不管是 double 类型,还是 float 类型,默认小数点后都有 6 位
  • 我们可以用 %.f 的方式来控制打印,如 %.3f 就是只打印到小数点后 3 位

答案只有一个:浮点型在内存中的存储方式和 int 类型完全不同!

浮点型如何在内存中存放?

根据国际标准 IEEE(电气和电子工程协会) 754,任意一个二进制浮点数 V 可以表示成下面的形式:

  • (-1)^S 表示符号位,当 S=0,V 为正数;当 S=1,V 为负数。

  • M 表示有效数字,大于等于 1,小于 2。

  • 2^E 表示指数位。

image-20220110141551509

十进制 & 二进制的科学计数法

我们在小学就学到过,1.234×10^2=123.4

而二进制中,其实就是把底数的 10 变成了 2,1.011*2^2=101.1

记住以下这个结论即可

二进制码 M 乘以 2 的 n 次方,相当于将二进制码 M 的小数点向右移动 n 位

S\M\E 如何判断?

二进制的小数部分 0.1 表示十进制的 0.5,即 2 的 - 1 次方。根据小数点开始往右数的位数,依次为十进制 2 的 - 1、-2、-3… 次方

我们以 5.5 为例,它的二进制是 101.1,带入上方提到的公式,相当于

和上面的公式比对,我们可以读出来 S=0,M=1.011,E=2

在之前的学习中,我们知道 float 类型占用 4 个字节的空间,而 double 类型则是 8 个字节

浮点类型的内存空间示意图

image-20220425204828194

float 类型的 S\E\M 被分区存放在这 4 个字节的内存空间中

同理,double 类型的 S\E\M 也是分区存放,它的有效数字长于 float 类型

对于 64 位的浮点数,最高的 1 位是符号位 S,接着的 11 位是指数 E,剩下的 52 位为有效数字 M

image-20220425204844636

IEEE754 对 M 的特殊规定

因为是二进制数,1≤M<2,而 M 可以写成 1.xxxxxx 的形式,其中 xxxxxx 是小数部分

IEEE 754 规定,在计算机内部保存 M 时,默认这个数的第一位总是 1,因此可以被舍去,只保存后面的 xxxxxx 部分

比如保存 1.01 的时候,只保存小数点后的 01,等到读取的时候,再把第一位的 1 加上去

这样做的目的是节省 1 位有效数字。以 32 位浮点数为例,留给 M 只有 23 位,将第一位的 1 舍去以后,等于可以保存 24 位有效数字。

我在学习的时候,关于这个 24位有效数字曾产生了疑惑。

实际上它并不难理解:

在内存中,32 位浮点数的 M 有 23 位的空间,如果我们保存了小数点前面的 1,就只能保存小数点后 22 位的内容。

但如果我们省略 1,只保留小数点后的内容,那不就能保存到小数点后第 23 位了吗?再加上原来小数点前的 1,不就是 24 位有效数字了!

IEEE754 对指数 E 的特殊规定

E 是一个无符号整数 (unsigned int)

  • 如果 E 为 8 位,它的取值范围是 0-255
  • 如果 E 为 11 位,它的取值范围是 0-2047

可是科学计数法里面的 E 是可以出现负数的。

所以 IEEE754 规定,存入内存时 E 的真实值必须再加上一个中间数

  • 8 位的 E,中间数是 127
  • 11 位的 E,中间数是 1023

例:2^10 的 E 是 10,所以保存为 32 位浮点数的时候,E 必须保存为 10+127=137,即 10001001

保存为 64 位浮点数的时候,E 保存为 10+1023=1033,即 10000001001

①当 E 不为全 0 或全 1 时

浮点数采用下面的规则来进行存放:

内存中指数 E 的计算值减去 127 (或 1023),得到 E 的真实值,再将有效数字 M 前面加上第一位的 1

以 32 位浮点数举例

0.5 的二进制形式为 0.1。科学计数法中整数部分必须为 1,小数点应右移一位

则为 1.0*2^(-1),E 的真实值为 -1,存放在内存中为 -1+127=126(01111110)

M 存放小数点后的 0,补全 23 位,全为 0

这时候 0.5 的二进制表现形式就是

c
1
0 01111110 00000000000000000000000

对应 S、E、M 的部分如下表所示

SEM
00111111000000000000000000000000

②当 E 为全 0 时

这时,浮点数的指数 E 等于 1-127(或者 1-1023)即为真实值

有效数字 M 不再加上第一位的 1,而是还原为 0.xxxxxx 的小数

这样做是为了表示 ±0,以及接近于 0 的很小的数字

③当 E 为全 1 时

当 E 为全 1 时,原 E 为 128,数字非常大,相当于无穷大

这时,如果有效数字 M 全为 0,表示 ± 无穷大(正负取决于符号位 s)

解释开篇代码

回到开头

整形 9 的原码如下

c
1
00000000 00000000 00000000 00001001

当我们将它强制存放到 float 类型的指针中时

SEM
0000000000000000 00000000 00001001

解码出来就是
$$
(-1)^00.0000000 00000000 000010012^{-126}
$$
这是一个很小的数字,远小于 float 类型默认的小数点后六位,所以 printf 打印的是 0.000000

c
1
2
3
4
5
int n;
float* pfloat = (float*)&n;
*pfloat = 9.0; // 以浮点数形式存入了整形n的地址空间
printf("num的值为:%d\n", n);
printf("*pfloat的值为:%f\n", *pfloat);

这里 9.0 就是以浮点数的形式存入 float 指针的

  • 9.0 十进制
  • 1001.0 二进制
c
1
2
3
S=0, M=1.001, E=3
二进制码
0100 0001 0001 0000 0000 0000 0000 0000

开启调试,在内存框中查看 n 的地址如下

image-20220110160103813

41100000
0100 00010001 00000000 00000000 0000

正好对应了浮点数 9.0 在内存中存放的二进制码

最后 n 以 %d 整形的方式打印出来,就是我们看到的 1091567616

结语

考试周快要结束啦!寒假将开始新的代码学习

终于补上了之前欠下的博客了,当作是一种复习吧,的确有不少东西已经忘记的差不多了😥

感谢你看到最后,点个赞再走吧!