初学C语言:浮点数的存储方式

发布于:2023-01-13 ⋅ 阅读:(507) ⋅ 点赞:(0)

前言:本人为C语言初学者,学识尚浅,研究程度存在很大的局限性,眼界很窄。以下所有观点仅代表个人见解和思路,各位游刃有余的前辈可以给予批评和指正!各位与鄙人同路的学子可相互探讨、发表看法,交换观点!

(本文仅用于理解浮点数的储存,不会涉及其规定标准、浮点数的由来等无关内容)

其实这一切,都要从这串代码说起:

#define _CRT_SECURE_NO_WARNINGS 1

#include <stdio.h>

int main()
{
	int a = 9;
	float* p = &a;
	printf("%d\n", a);
	printf("%f\n", *p);

	*p = 9.0;
	printf("%d\n", a);
	printf("%f\n", *p);

	return 0;
}

或许你这样认为,打印的应该是:

9   9.0   9   9.0

(因为我一开始也是这样以为的)

所以先在自己心里留个答案,那我们先来见证一下结果:

哈,很惊喜吧!接下来,我们就不得不探讨浮点数怎样在内存中储存:

对于整数来说,它们采取补码的方式储存,大于等于0的整数原反补码相同,小于0的整数补码 = 原码按位取反(符号位不变)+ 1,而 int 类型占4个字节。

浮点数分为:单精度浮点数(float - 4byte),双精度浮点数(double - 8byte)

而它们如何利用这些比特位来表示小数呢?

首先还是要先奉上公式:

V = (-1)^S * M * 2^E

先不要着急......V即表示浮点数的意思,而后面自然就是计算方法,我们逐步剖析

S

如果你对高中数学还有那么一点印象,你会发现高中很喜欢用 (-1)^n 来表示正数或者负数,因为n为正数时取偶数其值为1,取奇数时其值为1

而这里的 (-1)^S 岂不是有些神似?因为二进制位只有0和1两个数字,刚好 S = 1 时其值为 -1,S = 0 时其值为 1,用一个二进制位即可表示正负,这也恰恰和整数储存中用首位二进制位来表示正负不谋而合!所以对于S而言,我们知道,它应该是用来决定符号位的,且它需要一个bit位

M

我们先从十进制说起,从初中就开始接触的科学计数法,告诉我们,例如550,我们可以表示成5.5*10^2,或是7800可以表示成 7.8*10^3

细心的你应该发现,任何一个十进制数,我们都能表示成:A * 10^N 

而这里A的取值范围永远是:[1,10)

转换成二进制,那么这里M的取值不就应该是[1,2)吗?

所以M的意思我们也理解了,它作为浮点数的基数,永远是大于等于1且小于2的

所以,这个数永远都是1开头的1.xxxxxx,而在存储的时候,这个1就是可有可无的了,因为只要拿出来用的时候再给它加上1就好,这样对空间的利用率更高!

对于float类型,规定对M的储存分配23个bit位空间,对于double类型,分配52个bit位空间

E

在对M的解析中我们就已经知道E就等同于其中的N,如果55在10进制中表示成 5.5*10^1,那么5.5用二进制怎么表示?小数点前面的5转换成 101 ,小数点后面的呢?其实是1,合起来就是101.1

该如何理解?

对于十进制而言,5.5其实等于:5*10^0 + 5*10^-1,小数点前的5权重是 10^0,小数点后的5权重是 10^-1

对于二进制而言,我们分析其权重,会发现,101,最后一位权重是 2^0,倒数第二位是 2^1,倒数第三位是 2^2,以此类推,如果是小数点后,也就是 2^-1,正好就是0.5,也就是101.1的由来

而它终究会被转换成:1.011*2^2,而这个次方的2,就是由E储存的,且E被规定为无符号型

(其实次方就是移位操作,上述数中2次方即把小数点像右移动两位,在十进制中也是这个道理)

对于float类型,规定对E的储存分配8个bit位空间,对于double类型,分配11个空间

现在,表达式中所有的符号我们都了解了,我们用图示方法表示:

 

 

概况说完了,我们再来陈述一下细节:

拿6.5为例,以float形式储存:

6.5 = 110.1 = 1.101 * 2^2

为正数,符号位S = 0

按道理,E应该储存2,但是E无符号型已在前文提及,如果出现一种情况:

0.5转换成二进制,也就是0.1 = 1.0 * 2^-1,这里的E却出现了负数,为了解决这一情况,规定E+127后再储存,127为0~255的中间值,如果是double,则需要加上1023(0~2048的中间值)

所以E中存放的应该是2+127 = 129 = 10000001

M是用来存放基数的,即1.101,其实我们知道,既然基数的取值范围是[1,2),那么按道理,小数点左边这一位是恒为1的,所以这里为了节省空间,规定仅对101进行储存,而1则做省略处理,如果需要拿出来用,加上1就好了,因为这适用于所有情况

所以M中存放的应该是101,而后面没有的,则自动填充0

即M为101 0000 0000 0000 0000 0000

合起来,6.5在内存中应该是:

0 10000001 10100000000000000000000

如果我们拆分然后换成16进制:

0100 0000 1101 0000 0000 0000 0000 0000

40 D0 00 00

那么,见证一下吧:

(这里倒序是因为小端储存) 

于是,我们回到了最开始我们的那串代码:

#define _CRT_SECURE_NO_WARNINGS 1

#include <stdio.h>

int main()
{
	int a = 9;
	float* p = &a;
	printf("%d\n", a);
	printf("%f\n", *p);

	*p = 9.0;
	printf("%d\n", a);
	printf("%f\n", *p);

	return 0;
}

int a = 9; 在内存中不就是:00 00 00 09 吗?(以正常顺序,方便理解)

对应的二进制为:0000 0000 0000 0000 0000 0000 0000 1001

如果硬是用float类型输出,就会被解释成:0 00000000 00000000000000000001001

但是float顶多输出小数点后6位,这个数字已经相当小了,所以我们看到的结果是:

0.000000(但其实不然,这里的E中8个0有额外释义,后文会释义,先不要着急...)

所以如果我们往a中存放9.0,是以浮点数形式存放的,所以存放的其实是:

0 10000010 00100000000000000000000

但是我们又以%d转换为整型输出,则其被解释为整型:

 

可以看到,十进制的表示就是:1091567616

这也就完美解释了我们上述代码运行的结果! 

其实,对于E的8位(float),有两种特殊情况:

1.全为0,如果全为0,可想而知,是加上127后全为0,那么这个数原来的E是-127,2的-127次方???你想想有多小,2的32次方就有43亿!所以,如果全为0,则默认E = 1 - 127 为真实值(或1 - 1023),且有效数字M不再加上第一位的1,这样是为了表示极小的数,换句话说,也就是无穷小,无限接近于0!

2.全为1,同理,全为1则为255,则原来的E为128,大的也可想而知,所以全为1时,该数表示无穷大或无穷小(取决符号位)

当然,如果你脑洞大开,你会发现,计算机无法精确的储存1.3这样的数字,所以计算机只会以最大精度输出(比如保留6位小数)!

比如1.3,转换成二进制是1.01001100110011001100110011......所以只能在23位舍0进1,然后再转换,计算机这一点还是多少有点尴尬~但是保留6位小数(float)仍然是1.300000!

最后,还是想提一嘴,如果你要把二进制序列转换成浮点数,M阶段的部分一定要补上1,除非是全0的情况!E的部分一定要减去127,除了全0默认为-126!

END


网站公告

今日签到

点亮在社区的每一天
去签到