目录
注:所有的[斜体]内容都是本人自己杜撰的概念,只代表个人理解,可能不能与科学规范的学术概念相吻合
所有的{ }包含的是一个格式
所有的" "包含的是一个对象实体
前言
从一个简单的程序里发现了点蹊跷
float a = 2.0e20+1.0;
float b = a - 2.0e20;
printf("%a",b);
┌──(ivaldi㉿kali)-[~/Desktop/C/float]
└─$ cd "/home/ivaldi/Desktop/C/float/" && gcc main.c -o main && "/home/ivaldi/Desktop/C/float/"main
0x1.d29cfp+41
基础知识
十进制浮点数与二进制浮点数的转换
首先抛开计算机存储的限制,我们来看看如何把一个[十进制浮点数]转化为一个[二进制浮点数]:
0.分离[十进制浮点数]为[符号位]和[不带符号位的十进制浮点数],这个[符号位]与[二进制浮点数]中所包含的[符号位]相同
1.分离[不带符号位的十进制浮点数]为[不带符号位的十进制浮点数的整数部分]和[不带符号位的十进制浮点数的小数部分]
2.[不带符号位的十进制数的整数部分]采用"除以2取余数,倒序相加"的方法,转化为[不带符号位的二进制数的整数部分]
3.[不带符号位的十进制数的小数部分]采用"乘以2取整数,顺序相加"的方法,转化为[不带符号位的二进制数的小数部分]
4.按照{[符号位]+[不带符号位的二进制数的整数部分]+[小数点] "."+[不带符号位的二进制数的小数部分]}的格式拼接在一起就变成了[二进制浮点数]
下面是一个简单的例子:
[十进制浮点数] "-4.8125" 转化为 [二进制浮点数]
(0)[十进制浮点数] "-4.8125" 分成 [符号位] "-" 和 [不带符号位的十进制浮点数] "4.8125"
(1)[不带符号位的十进制浮点数] "4.8125" 分成 [不带符号位的十进制浮点数的整数部分] "4和[不带符号位的十进制浮点数的小数部分] "0.8125"
(2)[不带符号位的十进制浮点数的整数部分] "4" 转化为[不带符号位的二进制数的整数部分] "100"
4 除以 2 = 2 ...... 0
2 除以 2 = 1......0
1 除以 2 = 0......1
得到商为0就没必要继续除了,如果继续除,得到的商和余数都是0,就进入无限循环了
把依次得到的余数,按照倒序,连接在一起,变成"100",这就是[不带符号位的二进制数的整数部分]的结果
(3)[不带符号位的十进制浮点数的小数部分] "0.8125" 转化为[不带符号位的二进制数的小数部分] "1101"
0.8125 乘以 2 = 0.625 + 1
0.625 乘以 2 = 0.25 + 1
0.25 乘以 2 = 0.5 + 0
0.5 乘以 2 = 0.0 + 1
得到小数部分为0就没必要继续乘了,如果继续乘,得到的积都是0,就进入无限循环了
把依次得到的整数部分,按照顺序,连接在一起,变成"1101",这就是[不带符号位的二进制数的小数部分]的结果
(4)按照{[符号位] "-" +[不带符号位的二进制数的整数部分] "100"+[小数点] "."+[不带符号位的二进制数的小数部分] "1101"}的格式,拼接在一起,就变成了[二进制浮点数]"-100.1101",当然,这也可以表示为"-1.001101*"
十六进制数浮点数
[十六进制数浮点数]是[二进制浮点数]的另一种表达形式,差别在于:
- ANSI-C中支持[十进制浮点数]格式的浮点型常量的书写,但是不支持[二进制浮点数]格式的浮点型常量的书写
- [二进制浮点数]只是计算机硬件层面的一种表现方式和原理体现
- C99标准中新增加了对[十六进制数浮点数]格式的浮点型常量的书写的支持
- [十六进制数浮点数]本质还是[二进制浮点数],只是[二进制浮点数]的另一种表达形式
将一个[二进制浮点数]转化为[十六进制数浮点数]的步骤:
0.将[二进制浮点数]拆解成[符号位]+[不带符号位的二进制浮点数]
1.将[不带符号位的二进制浮点数]拆解成[不带符号位的二进制浮点数的整数部分]+[小数点]+[不带符号位的二进制浮点数的小数部分]
2.[不带符号位的二进制浮点数的整数部分]从右往左,每4个一组,不足补0,转化为十六进制数,变成[不带符号位的十六进制数浮点数的整数部分]
3.[不带符号位的二进制浮点数的小数部分]从左往右,每4个一组,不足补0,转化为十六进制数,变成[不带符号位的十六进制数浮点数的小数部分]
4.按照{[十六进制前缀]+[符号位] +[不带符号位的十六进制数浮点数的整数部分]+[小数点]+[不带符号位的十六进制数浮点数的小数部分]}的格式,合成[十六进制数浮点数]
还是上面那个简单的例子:
"-100.1101" -> "-"+"100"+"."+"1101" -> "-"+"0100"+"."+"1101" ->""0x"+"-"+"8"+"."+"d" -> "0x-8.d"
浮点数的存储
浮点数存储的格式
再来看看浮点数在计算机中是如何进行存储的:
对于32bits的float型浮点数,其各个位存储的数据如下
| 31 | 30 | 29 | 28 | 27 | 26 | 25 | 24 | 23 | 22 | 21 | 20 | 19 | 18 | 17 | 16 | 15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
| 符号位 | 阶数位 | 尾数位 | |||||||||||||||||||||||||||||
对于64bits的double型浮点数,其各个位存储的数据如下
| 63 | 62 | 61 | 60 | 59 | 58 | 57 | 56 | 55 | 54 | 53 | 52 | 51 | 50 | 49 | 48 | 47 | 46 | 45 | 44 | 43 | 42 | 41 | 40 | 39 | 38 | 37 | 36 | 35 | 34 | 33 | 32 | 31 | 30 | 29 | 28 | 27 | 26 | 25 | 24 | 23 | 22 | 21 | 20 | 19 | 18 | 17 | 16 | 15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
| 符号位 | 阶数位 | 尾数位 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
规范化
为了方便浮点型数值在计算机中的存储,一个浮点数值在计算机中只能有一种表达形式,IEEE标准规定,一个[不带符号位的二进制数]需要规范化为{1.xxxx *
}的形式,其中,n为一个[有符号的十进制数]
规范化操作有两种方式:
左规:这是默认的规范化操作方式,每次将尾数位左移1位,并将阶数位-1
右规:只有当浮点数计算出现进位时,才会右规,将尾数位右移1位,并将阶数位+1.一次规范化中至多发生一次右规
比如:[十进制浮点数]"+2.0e+20"转化为[二进制浮点数]为"+10101101011110001110101111000101101011000110001000000000000000000000.0"
其[规范化的二进制浮点数]为"+1.01011010111100011101011110001011010110001100010000000000000000000000*"
其[规范化的十六进制数浮点数]为"0x1.5af1d78b58c4p67"
这里有几个小知识点要进行补充和说明:
0.[十进制浮点数]使用e/E来表示10的次方,[二进制浮点数]使用p/P来表示2的次方
1.表示次方的n都是一个[有符号的十进制数]
2.前面说了,[十六进制数浮点数]本质上还是[二进制浮点数],p/P后面接的n次方,指的是在二进制下小数点的位移,而不是十六进制下
3.可以观察到,[规范化的不带不带符号的二进制浮点数]除了0.0,其整数部分永远为1,所以计算机存储时,会忽略这个整数部分,只将小数部分存入尾数位
4.理论上,经过规范化处理的浮点数,其小数部分就是实际存储在计算机浮点数的尾数部分的二进制码,但是现实是,因为存储尾数的空间有限,有时因为尾数位过长,而不能全部存进去,此时就会进行舍入处理.
舍入处理
浮点数字的默认舍入方式是就近舍入,相当于我们常说的"四舍五入",这里我们将可以存入的位称为[有效位],无法存入的位称为[多余位]:
0.若[多余位]首位为0,则直接省去[多余位]
1.若[多余位]首位为1,且其余位不全为0,则[有效位]加1
2.若[多余位]等于1000....0000,且[有效位]末位为1,则[有效位]加1
3.若[多余位]等于1000....0000,且[有效位]末位为1,则直接省去[多余位]
这里还是拿上面的2.0E20举例子,理论上需要存入尾数位的二进制码为"0101101011110001110101111000101101011000110001"
对于64bits的double型内存空间,尾数位为51bits,因此会将原二进制码截断成[有效位]"010110101111000111010111100010110101100011000100000"和[多余位]"0000000000000000",[多余位]会被直接舍去.这时,尾数位存储的二进制码为:010110101111000111010111100010110101100011000100000
而对于32bits的float型内存空间,尾数位为23bits,因此会将原二进制码截断成[有效位]"01011010111100011101011"和[多余位]"11000101101011000110001000000000000000000000",[有效位]+1变成[有效位]"01011010111100011101100",这时,尾数位存储的二进制码为:01011010111100011101100
溢出操作
[规范化的二进制浮点数]的指数,理论上是可以直接以[有符号数]的补码形式直接存的,补码也不会产生歧义,但是为了方便浮点数之间进行比较,规定采用阶码(补码+偏码)的方式来存储指数
- 对于32bits的float型浮点数,其阶数位为8位,规定其偏码为
-1 = 127,即0B 0111 1111
- 对于64bits的double型浮点数,其阶数位为11位,规定其偏码为
-1 = 1023,即0B 011 1111 11111
2.0E20对应的[规范化的二进制浮点数]的指数为67,其补码为0B 0100 0011,当然也可以是0B 000 0100 0011
- 对于32bits的float型浮点数,67对应阶码为 0B 0111 1111+ 0B 0100 0011 = 0B 1100 0010
- 对于64bits的double型浮点数,67对应阶码为 0B 011 1111 1111+ 0B 000 0100 0011 = 0B 100 0100 0010
对于每种类型,操作系统都保留了两个阶码000...000和111...111,用来判断溢出操作,我们先看看这两个阶码原本对应什么十进制数
- 对于32bits的float型浮点数,11111111 - 01111111 = 1000 0000,对应十进制-128, 0000 0000 - 0111 1111 = 1000 0001,对应十进制-127
- 对于64bits的double型浮点数,111 1111 1111 - 011 1111 1111 = 100 0000 0000,对应十进制-1024, 000 0000 0000 - 011 1111 1111 = 100 0000 0001,对应十进制-1023
也就是说,实际上,指数并不能取到最低的两个,这么设计的理由是希望一个类型能表示的数字的范围尽可能的大
系统保留的两个阶码被用来进行溢出的判断,浮点数的溢出只根据阶码来判断,这里只拿float来讲讲,float的阶码范围为0000 0001到 1111 1110
- 若一个有效的浮点数,其阶码为0000 0001,现在要把这个数除以2,我们从[规范化的二进制浮点数]格式可以看出,这个数的尾数部分不会发生改变,但是阶码需要减1,就变成了0000 0000,操作系统就会判定这个数字出现了溢出,因为是超过了最小能表示的值,所以叫做下溢
- 若一个有效的浮点数,其阶码为1111 1110,现在要把这个数乘以2,我们从[规范化的二进制浮点数]格式可以看出,这个数的尾数部分不会发生改变,但是阶码需要加1,就变成了1111 1111,操作系统就会判定这个数字出现了溢出,因为是超过了最大能表示的值,所以叫做上溢
- 同理的,只要一个[规范化的二进制浮点数],其指数只要超过了阶码所能表示的范围,就是溢出.操作系统对于溢出采取了一些规定如下:
- 若浮点数的符号为正,且溢出方式为上溢,称为正上溢,记为+
,表示为inf
- 若浮点数的符号为负,且溢出方式为上溢,称为负上溢,记为-
,表示为-inf
- 若浮点数的符号为正,且溢出方式为下溢,称为正下溢,记为0
- 若浮点数的符号为正,且溢出方式为下溢,称为负下溢,记为0
浮点数的计算
现在再来回顾一开始这个非常简单的程序
1.我们先把2.0E20和1.0转化为 [规范化的二进制浮点数]:
2.0E20 => 1.01011010111100011101011110001011010110001100010000000000000000000000*
1.0 => 1.0*
2.舍入处理,并存储为内存中的临时数据
2.0E2
| 63 | 62 | 61 | 60 | 59 | 58 | 57 | 56 | 55 | 54 | 53 | 52 | 51 | 50 | 49 | 48 | 47 | 46 | 45 | 44 | 43 | 42 | 41 | 40 | 39 | 38 | 37 | 36 | 35 | 34 | 33 | 32 | 31 | 30 | 29 | 28 | 27 | 26 | 25 | 24 | 23 | 22 | 21 | 20 | 19 | 18 | 17 | 16 | 15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
| 0 | 1 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 1 | 0 | 1 | 1 | 0 | 1 | 0 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 1 | 1 | 1 | 0 | 1 | 0 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 1 | 0 | 1 | 1 | 0 | 1 | 0 | 1 | 1 | 0 | 0 | 0 | 1 | 1 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 |
| 符号位 | 阶数位 | 尾数位 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
1.0
| 63 | 62 | 61 | 60 | 59 | 58 | 57 | 56 | 55 | 54 | 53 | 52 | 51 | 50 | 49 | 48 | 47 | 46 | 45 | 44 | 43 | 42 | 41 | 40 | 39 | 38 | 37 | 36 | 35 | 34 | 33 | 32 | 31 | 30 | 29 | 28 | 27 | 26 | 25 | 24 | 23 | 22 | 21 | 20 | 19 | 18 | 17 | 16 | 15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
| 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 符号位 | 阶数位 | 尾数位 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
3.对阶
浮点数进行计算前,先要把让两个浮点数的阶数相同,且为向阶数大的一方对齐,操作为:连续右移阶数较小者的尾数位,每位移一次,阶数加1,直至相等
这里需要注意的是1.0原来含有隐藏位1,在右移过程中,该隐藏位也会纳入位移的范围,即第一次右移后,1.0对应的尾数位变为 1000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000,隐藏位变为0,对阶完成的时候,1.0的阶数变成10001000010,而由于右移次数过多,尾数为依然全为0,仿佛没有发生变化
这里另外还有提一句,虽然不是每种编译器都支持扩展双精度类型(long double),扩展双精度类型不存在1位隐藏位,它的尾数位是多少,这个数就是多少,说起来还是挺喜欢这种"直白"的类型的
4.尾数计算,得到新的尾数 0101 1010 1111 0001 1101 0111 1000 1011 0101 1000 1100 0100 0000,将这个尾数,阶数位和符号位存入变量a对应的内存,这时,由于a为float类型,而浮点常量默认为double类型,尾数位要进行截断,阶数位要进行转化,因此要进行舍入处理和溢出判断
根据之前的内容,尾数位舍入为0101 1010 1111 0001 1101 100,阶数位转化为:1100 0010 ,即变量a所在内存空间中的每个位为:
| 31 | 30 | 29 | 28 | 27 | 26 | 25 | 24 | 23 | 22 | 21 | 20 | 19 | 18 | 17 | 16 | 15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
| 0 | 1 | 1 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 1 | 0 | 1 | 1 | 0 | 1 | 0 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 1 | 1 | 1 | 0 | 1 | 1 | 0 | 0 |
| 符号位 | 阶数位 | 尾数位 | |||||||||||||||||||||||||||||
5.第二条语句中的详细操作后面就不赘述太多,简要说明一下:
将变量a对应内存的float类型的浮点数读出,重新分配临时空间,将其转化为一个double类型的常量,包括阶码的转化,尾数位补0
将浮点常量2.0E20,存储为一个临时的double类型常量
对阶,由于阶码本来就相等,就不进行操作
由于是减法操作,2.0E20对应内存的[有效位](隐藏位+尾数位)需要整体作为原码,变成补码,即由010101101011110001110101111000101101011000110001000000变成101010010100001110001010000111010010100111001111000000
尾数加减(其实我更愿意称呼为有效位加减)
01. 0101 1010 1111 0001 1101 1000 0000 0000 0000 0000 0000 0000 0000
+ 10. 1010 0101 0000 1110 0010 1000 0111 0100 1010 0111 0011 1100 0000
---------------------------------------------------------------------------------------------------------------------
00. 0000 0000 0000 0000 0000 0000 0111 0100 1010 0111 0011 1100 0000
规范化,有效位右移26位,有效位变成01.11 0100 1010 0111 0011 1100 0000...(后面的0懒得写了),阶码-26
判断是否溢出,这里粗略看一下是没有溢出的,详细的也可以自己算算
存储,将上述计算出来的double类型结果,储存在一个float类型中,最终存储在变量b中的各个位为:0 1010 1000 11 0100 1010 0111 0011 1100 0
| 31 | 30 | 29 | 28 | 27 | 26 | 25 | 24 | 23 | 22 | 21 | 20 | 19 | 18 | 17 | 16 | 15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
| 0 | 1 | 0 | 1 | 0 | 1 | 0 | 0 | 0 | 1 | 1 | 0 | 1 | 0 | 0 | 1 | 0 | 1 | 0 | 0 | 1 | 1 | 1 | 0 | 0 | 1 | 1 | 1 | 1 | 0 | 0 | 0 |
| 符号位 | 阶数位 | 尾数位 | |||||||||||||||||||||||||||||
最后,我们也不难发现,这个结果正是我们上面得到的结果,至于阶码就自己算算吧
1101 0010 1001 1100 1111 000
------------------------------------------
d 2 9 c f 0
本文深入探讨浮点数的基础知识,包括十进制与二进制、十六进制之间的转换,以及浮点数在计算机中的存储格式、规范化、舍入处理和溢出操作。通过实例详细解释了浮点数计算过程,帮助读者理解浮点数在编程中的工作原理。
2801

被折叠的 条评论
为什么被折叠?



