IEEE-754标准
目前世界上使用最为广泛的小数表示方法是浮点数表示法,而浮点数通用的算术标准是IEEE-754标准。什么是IEEE?
IEEE 电气和电子工程师协会(IEEE,全称是Institute of Electrical and Electronics Engineers)是一个美国的电子技术与信息科学工程师的协会,是世界上最大的非营利性专业技术学会,其会员人数超过40万人,遍布160多个国家。IEEE致力于电气、电子、计算机工程和与科学有关的领域的开发和研究,在航空航天、信息技术、电力及消费性电子产品等领域,已制定了900多个行业标准,现已发展成为具有较大影响力的国际学术组织
—— 引自百度百科
之所以要写这么一篇文章,是因为我想要搞懂 C语言对double
和float
的表示和存储细节。之前自己弄懂了,不过由于没有对文件进行备份,导致我的实验的代码和笔记都被误删了,连带被误删的还有一篇探究字节序(大小端)的笔记和代码,以及一篇关于开平方根算法和编码的笔记(X86架构有开平方根的及其指令,超快)。这些使我意识到了将笔记转化为文章并分享到网上的必要性。
这篇文章并非细致认真的标准解读手册,只是想探究一下double
和float
的二进制存储序列。
C 中的浮点数表示
我们知道,C语言的浮点数分为单精度和双精度,单精度的float
采用位二进制(
字节)来存储,而双精度的
double
使用位,另外还有一种占
位(
个字节)的临时数。
一个浮点数的存储分为3个部分,分别是符号位
,阶码
和尾数
,那么这三部分是如何组合而成为一个浮点数整体的呢?
对于一个位浮点数,我们可以用下面的这张示意图来表示它的各个部分的长度及顺序。其中一个等号
=
表示一个二进制位,|
表示隐含的边界。
|=|===========|===================================================|
|s|-exponent--|--------------------mantissa-----------------------|
上面的图示中,s
(sign)为符号位,占 bit,用来表示整个
double
的正负性;中间部分exponent
是指数部分,即阶码,占 bit;最后的也是最长的一部分
mantissa
,尾数,占位,它的长度直接影响力浮点数的精度。
下面是这三个部分的具体细节
-
符号位跟整数一样,符号位取
表示无符号,为正数;取
表示有符号,为负数。
-
指数部分采用的是移码表示法并且采用的是余1023码,也就是说,指数部分的
位没有符号位,在计算时,要先将这
位看作一个正整数,然后减掉
之后才得真正的指数。
-
关于尾数部分,有一点需特别注意,尾数部分的值总是
1.M
,而1.M
中的1
是被隐藏了的。为什么呢?这样做有什么意义?
我们知道,一个十进制数采用科学计数法表示的话,形式是其中
。类推一下,就可以知道,一个
进制的科学计数法,小数的整数部分的取值范围是
,所以在二进制中,小数部分的取值范围是
,这个范围内的实数,整数部分都是
,所以这个
1
是大家共有的,于是就没有存储的必要性了,因为
你爱,或者不爱
爱就在那里,不增不减
你存,或者不存
它就在那里,不大不小
上面讲述了double
类型的存储。一个double
占各二进制位,而一个
float
则占用位,包括
位符号位、
位指数位和
位尾数位。
提取一个double
的各个部分
下面,我们用C语言编写一个程序来打印一下一个double
的各个部分的二进制及十进制。相信理解了这段代码,你就真的理解浮点数的表示了。
#include <stdio.h>
#include <assert.h>
#define NM (1LL << 63) /* negative most */
#define PM ~NM /* positive most */
#define LL(d) *((long long*)&(d))
#define EZ(d) LL(d) &= (PM >> 1), LL(d) |= (1023LL << 52)
int sign(double d) { return (LL(d) >> 63) & 1LL; }
int exponent(double d) { return (LL(d) >> 52) & 0x7ff; }
// `EZ(d) -> LL(d) &= (PM >> 1), (LL(d) >> 52) & 0x7ff;`
// 将`d`的指数部分的$11$位填上$1023$(低$10$位全$1$,最高位为$0$)
// 因为采用的是余$1023$码,所以这条语句的目的是将指数部分变为$0$
double mantissa(double d) { return EZ(d), d; }
#define sign(d) (sign(d)? -1 : 1)
#define exponent(d) (exponent(d) - 1023)
/* print binary of a double */
void printbd(double d)
{
/* from left to right */
printf("%+g =\n", d);
printf("%4c", 32);
for (int i = 0; i < 64; ++i) {
if (i == 0 || i == 1 || i == 12)
putchar('|');
// 这里只能用右移,因为 (long long -> int) 要截断到低32位
putchar(((LL(d) >> (63 - i)) & 1LL) + '0');
}
printf("|\n");
printf("%4c", 32);
printf("%+d * %g * 2^(%d)\n", sign(d), mantissa(d), exponent(d));
}
int main()
{
assert(sign(+0.5) == +1);
assert(sign(-0.5) == -1);
printbd(-0.05);
printbd(+0.05);
printbd(-0.5);
printbd(+0.5);
printbd(-1.0);
printbd(+1.0);
printbd(-2.0);
printbd(+2.0);
printbd(-9.0);
printbd(+9.0);
printbd(-10.0);
printbd(+10.0);
return 0;
}
另外一些需要注意的细节
因为采用的是移码表示法,所以不像补码表示法,可以直接从二进制判断一个数的大小。指数部分全为时,指数部分的取值最大。
对于正负无穷及不合法的运算结果,IEEE标准规定
- 如果指数部分是
并且尾数的小数部分是
,则表示
(符号位相关);
- 如果指数部分全为
,并且尾数的小数部分是
,则表示
;
- 如果指数部分全为
,并且尾数的小数部分不为
,那么表示
Not a Number
,即。
附录
前文代码的运行结果
-0.05 =
|1|01111111010|1001100110011001100110011001100110011001100110011010|
-1 * 1.6 * 2^(-5)
+0.05 =
|0|01111111010|1001100110011001100110011001100110011001100110011010|
+1 * 1.6 * 2^(-5)
-0.5 =
|1|01111111110|0000000000000000000000000000000000000000000000000000|
-1 * 1 * 2^(-1)
+0.5 =
|0|01111111110|0000000000000000000000000000000000000000000000000000|
+1 * 1 * 2^(-1)
-1 =
|1|01111111111|0000000000000000000000000000000000000000000000000000|
-1 * 1 * 2^(0)
+1 =
|0|01111111111|0000000000000000000000000000000000000000000000000000|
+1 * 1 * 2^(0)
-2 =
|1|10000000000|0000000000000000000000000000000000000000000000000000|
-1 * 1 * 2^(1)
+2 =
|0|10000000000|0000000000000000000000000000000000000000000000000000|
+1 * 1 * 2^(1)
-9 =
|1|10000000010|0010000000000000000000000000000000000000000000000000|
-1 * 1.125 * 2^(3)
+9 =
|0|10000000010|0010000000000000000000000000000000000000000000000000|
+1 * 1.125 * 2^(3)
-10 =
|1|10000000010|0100000000000000000000000000000000000000000000000000|
-1 * 1.25 * 2^(3)
+10 =
|0|10000000010|0100000000000000000000000000000000000000000000000000|
+1 * 1.25 * 2^(3)
网友评论