美文网首页
理解C语言浮点数的存储

理解C语言浮点数的存储

作者: ABleaf | 来源:发表于2019-12-08 11:10 被阅读0次

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位,它的长度直接影响力浮点数的精度。

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

  • 符号位跟整数一样,符号位取0表示无符号,为正数;取1表示有符号,为负数。

  • 指数部分采用的是移码表示法并且采用的是余1023码,也就是说,指数部分的11位没有符号位,在计算时,要先将这11位看作一个正整数,然后减掉1023之后才得真正的指数。

  • 关于尾数部分,有一点需特别注意,尾数部分的值总是 1.M,而1.M中的1是被隐藏了的。为什么呢?这样做有什么意义?
    我们知道,一个十进制数采用科学计数法表示的话,形式是 d \times 10^e 其中d \in [1.0, 10)。类推一下,就可以知道,一个k进制的科学计数法,小数的整数部分的取值范围是[1.0, k),所以在二进制中,小数部分的取值范围是[1.0, 2),这个范围内的实数,整数部分都是1,所以这个1是大家共有的,于是就没有存储的必要性了,因为

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

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

上面讲述了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并且尾数的小数部分是0,则表示±0(符号位相关);
  • 如果指数部分全为1,并且尾数的小数部分是0,则表示±\infty
  • 如果指数部分全为1,并且尾数的小数部分不为0,那么表示Not a Number,即NaN

附录

前文代码的运行结果

-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)

相关文章

  • 理解C语言浮点数的存储

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

  • IEEE 二进制浮点数的表示

    浮点数 在 C 语言中,有两种存储浮点数的方式,分别是 float 和 double ,当然了还有long dou...

  • 基本数据格式在内存中存储的格式

    1. 基本数据格式在内存中存储的格式[1] 1.1 浮点数类型(Float&Double) C语言和C#语言中,对...

  • iOS:言简意赅理解static与const

    通过c语言来理解static及const 一:static的理解 (1)从存储空间来理解分为:程序区、静态�存储区...

  • C 内存管理

    1、存储类别 C 语言提供了多个不同的存储类别在内存中存储数据。要理解这些存储类别,我们先来理解一些概念。 1.1...

  • long double可不一定精度比double高♂哟

    相信很多人学C语言时, 对long double的印象就是, 它能存储精度比double更高的浮点数.但事实上并不...

  • C语言变量的内存实质

    一、先来理解C语言中变量的实质 要理解C指针,我认为一定要理解C中“变量”的存储实质,所以我就从“变量”这个东西开...

  • C/C++ 变量存储位置

    本文主要介绍了C语言中各个变量存储位置,有助于学习C语言的各位能够更好的理解它们,希望对大家的C语言的学习有所帮助...

  • 5、小数类型及复习

    小数也叫浮点数,现实中的小数在C语言中存储的话分为两种,分别是float和double 进行输出的话可以用%f,%...

  • javascript中0.1+0.2背后的原理

    浮点数的存储 首先要搞清楚javascript如何存储小数的,它和其他语言存储,javascript中的整数和小数...

网友评论

      本文标题:理解C语言浮点数的存储

      本文链接:https://www.haomeiwen.com/subject/fzfmgctx.html