理解C语言浮点数的存储

2019-12-08  本文已影响0人  ABleaf

IEEE-754标准

目前世界上使用最为广泛的小数表示方法是浮点数表示法,而浮点数通用的算术标准是IEEE-754标准。什么是IEEE?

IEEE 电气和电子工程师协会(IEEE,全称是Institute of Electrical and Electronics Engineers)是一个美国的电子技术与信息科学工程师的协会,是世界上最大的非营利性专业技术学会,其会员人数超过40万人,遍布160多个国家。IEEE致力于电气、电子、计算机工程和与科学有关的领域的开发和研究,在航空航天、信息技术、电力及消费性电子产品等领域,已制定了900多个行业标准,现已发展成为具有较大影响力的国际学术组织

—— 引自百度百科

之所以要写这么一篇文章,是因为我想要搞懂 C语言对doublefloat的表示和存储细节。之前自己弄懂了,不过由于没有对文件进行备份,导致我的实验的代码和笔记都被误删了,连带被误删的还有一篇探究字节序(大小端)的笔记和代码,以及一篇关于开平方根算法和编码的笔记(X86架构有开平方根的及其指令,超快)。这些使我意识到了将笔记转化为文章并分享到网上的必要性。

这篇文章并非细致认真的标准解读手册,只是想探究一下doublefloat的二进制存储序列。

C 中的浮点数表示

我们知道,C语言的浮点数分为单精度和双精度,单精度的float采用32位二进制(4字节)来存储,而双精度的double使用64位,另外还有一种占80位(10个字节)的临时数。

一个浮点数的存储分为3个部分,分别是符号位阶码尾数,那么这三部分是如何组合而成为一个浮点数整体的呢?

对于一个64位浮点数,我们可以用下面的这张示意图来表示它的各个部分的长度及顺序。其中一个等号=表示一个二进制位,|表示隐含的边界。

|=|===========|===================================================|
|s|-exponent--|--------------------mantissa-----------------------|

上面的图示中,s(sign)为符号位,占 1 bit,用来表示整个double的正负性;中间部分exponent是指数部分,即阶码,占 11 bit;最后的也是最长的一部分mantissa,尾数,占52位,它的长度直接影响力浮点数的精度。

下面是这三个部分的具体细节

你爱,或者不爱
爱就在那里,不增不减

你存,或者不存
它就在那里,不大不小

上面讲述了double类型的存储。一个double64各二进制位,而一个float则占用32位,包括1位符号位、8位指数位和23位尾数位。

提取一个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;
}

另外一些需要注意的细节

因为采用的是移码表示法,所以不像补码表示法,可以直接从二进制判断一个数的大小。指数部分全为1时,指数部分的取值最大。
对于正负无穷及不合法的运算结果,IEEE标准规定

附录

前文代码的运行结果

-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)
上一篇 下一篇

猜你喜欢

热点阅读