深层次理解浮点型在内存中的存储

简介: 深层次理解浮点型在内存中的存储

1.什么是浮点数

       人们对于浮点数的理解大多是偏向于“小数”,但是这并不能解释浮点数这个名字。浮点数,顾名思义就是漂浮不定的,那怎么理解这个浮动的概念呢?

       其实,浮点数是采用科学计数法的方式来表示的,例如十进制小数 1.234,用科学计数法表示,可以有多种方式:


       1.234 == 0.1234*10^1

       1.234 == 1.234*10^0

       1.234 == 12.34*10^(-1)

       1.234 == 123.4*10^(-2)


       我们可以看见,小数点在整数值表达式中的是漂浮不动的,因此我们称之为浮点数。但是在计算机中,数值是以二进制存储的,那在计算机中的浮点数,又是如何存储的呢?


2.计算机中的浮点数

       对于我们程序员来说,说到浮点数,大多想到的是floatdoublelong double等浮点数类型,为什么要设置不同的精度来表示浮点数呢,他们在内存中的存储是有什么不同吗,以上就是我们今天需要探讨的内容

1.在编译器中对于浮点数的声明以及定义        

       对于浮点数中不同的类型,我们可以在编译器中打开相应的头文件查看定义,以下笔者以VS2022编译器举例,如下图打开定义

打开后我们就可以看见下面的代码以及相关注释

// float.h
//
//      Copyright (c) Microsoft Corporation. All rights reserved.
//
// Implementation-defined values commonly used by sophisticated numerical
// (floating point) programs.
//
#pragma once
#ifndef _INC_FLOAT // include guard for 3rd party interop
#define _INC_FLOAT
#include <corecrt.h>
#pragma warning(push)
#pragma warning(disable: _UCRT_DISABLED_WARNINGS)
_UCRT_DISABLE_CLANG_WARNINGS
_CRT_BEGIN_C_HEADER
#ifndef _CRT_MANAGED_FP_DEPRECATE
    #ifdef _CRT_MANAGED_FP_NO_DEPRECATE
        #define _CRT_MANAGED_FP_DEPRECATE
    #else
        #ifdef _M_CEE
            #define _CRT_MANAGED_FP_DEPRECATE _CRT_DEPRECATE_TEXT("Direct floating point control is not supported or reliable from within managed code. ")
        #else
            #define _CRT_MANAGED_FP_DEPRECATE
        #endif
    #endif
#endif
// Define the floating point precision used.
//
// For x86, results are in double precision (unless /arch:sse2 is used, in which
// case results are in source precision.
//
// For x64 and ARM, results are in source precision.
//
// If the compiler is invoked with /fp:fast, the compiler is allowed to use the
// fastest precision and even mix within a single function, so precision is
// indeterminable.
//
// Note that manipulating the floating point behavior using the float_control/
// fenv_access/fp_contract #pragmas may alter the actual floating point evaluation
// method, which may in turn invalidate the value of FLT_EVAL_METHOD.
#ifdef _M_FP_FAST
    #define FLT_EVAL_METHOD -1
#else
    #ifdef _M_IX86
        #if _M_IX86_FP >= 2
            #define FLT_EVAL_METHOD 0
        #else
            #define FLT_EVAL_METHOD 2
        #endif
    #else
        #define FLT_EVAL_METHOD 0
    #endif
#endif

 然后我们下滑可以找到具体的不同类型的申明和定义,其中对于floatdoublelong double等的最大值,最小值以及相关参数都做出了明确的说明

//-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
//
// Constants
//
//-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
#define DBL_DECIMAL_DIG  17                      // # of decimal digits of rounding precision
#define DBL_DIG          15                      // # of decimal digits of precision
#define DBL_EPSILON      2.2204460492503131e-016 // smallest such that 1.0+DBL_EPSILON != 1.0
#define DBL_HAS_SUBNORM  1                       // type does support subnormal numbers
#define DBL_MANT_DIG     53                      // # of bits in mantissa
#define DBL_MAX          1.7976931348623158e+308 // max value
#define DBL_MAX_10_EXP   308                     // max decimal exponent
#define DBL_MAX_EXP      1024                    // max binary exponent
#define DBL_MIN          2.2250738585072014e-308 // min positive value
#define DBL_MIN_10_EXP   (-307)                  // min decimal exponent
#define DBL_MIN_EXP      (-1021)                 // min binary exponent
#define _DBL_RADIX       2                       // exponent radix
#define DBL_TRUE_MIN     4.9406564584124654e-324 // min positive value
#define FLT_DECIMAL_DIG  9                       // # of decimal digits of rounding precision
#define FLT_DIG          6                       // # of decimal digits of precision
#define FLT_EPSILON      1.192092896e-07F        // smallest such that 1.0+FLT_EPSILON != 1.0
#define FLT_HAS_SUBNORM  1                       // type does support subnormal numbers
#define FLT_GUARD        0
#define FLT_MANT_DIG     24                      // # of bits in mantissa
#define FLT_MAX          3.402823466e+38F        // max value
#define FLT_MAX_10_EXP   38                      // max decimal exponent
#define FLT_MAX_EXP      128                     // max binary exponent
#define FLT_MIN          1.175494351e-38F        // min normalized positive value
#define FLT_MIN_10_EXP   (-37)                   // min decimal exponent
#define FLT_MIN_EXP      (-125)                  // min binary exponent
#define FLT_NORMALIZE    0
#define FLT_RADIX        2                       // exponent radix
#define FLT_TRUE_MIN     1.401298464e-45F        // min positive value
#define LDBL_DIG         DBL_DIG                 // # of decimal digits of precision
#define LDBL_EPSILON     DBL_EPSILON             // smallest such that 1.0+LDBL_EPSILON != 1.0
#define LDBL_HAS_SUBNORM DBL_HAS_SUBNORM         // type does support subnormal numbers
#define LDBL_MANT_DIG    DBL_MANT_DIG            // # of bits in mantissa
#define LDBL_MAX         DBL_MAX                 // max value
#define LDBL_MAX_10_EXP  DBL_MAX_10_EXP          // max decimal exponent
#define LDBL_MAX_EXP     DBL_MAX_EXP             // max binary exponent
#define LDBL_MIN         DBL_MIN                 // min normalized positive value
#define LDBL_MIN_10_EXP  DBL_MIN_10_EXP          // min decimal exponent
#define LDBL_MIN_EXP     DBL_MIN_EXP             // min binary exponent
#define _LDBL_RADIX      _DBL_RADIX              // exponent radix
#define LDBL_TRUE_MIN    DBL_TRUE_MIN            // min positive value
#define DECIMAL_DIG      DBL_DECIMAL_DIG

2.内存中对于浮点数的真正存储

我们接下来可以来看一段非常有意思的代码:

       对于下面的代码,很多初学者刚拿到手可能还是有点懵的,程序一共有4个打印,第一个是非常常规的对于整形9的打印,第二个是在使用指针解引用的方式打印,第三个好像是在用整形打印一个被浮点型指针修改过的值,最后一个好像和第二个没什么区别,那难道4个打印打印出来的全都是9?或者是整数9加上3个小数9?猜测过后,我们不妨运行起来试一试到底是怎么样的打印结果。


int main()
{
  int n = 9;
  float* pFloat = (float*)&n;
  printf("n的值为:%d\n", n);
  printf("*pFloat的值为:%f\n", *pFloat);
  *pFloat = 9.0;
  printf("num的值为:%d\n", n);
  printf("*pFloat的值为:%f\n", *pFloat);
  return 0;
}

 程序运行结果如下,结果非常的出人意料啊

       第一个和最后一个很好理解,用%d打印整形9,结果肯定是9。最后一个用%f打印9.0,结果肯定是浮点型9.000000。

       那中间俩个数是为什么呢,看起来根本无迹可寻啊。


       num 和 *pFloat 在内存中明明是同一个数,为什么浮点数和整数的解读结果会差别这么大? 要理解这个结果,就一定要搞懂浮点数在计算机内部的表示方法

3.浮点数存储规则于标准

我们已经知道,浮点数是采用科学计数法来表示一个数字的,它的格式可以写成这样:

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

S:符号位,取值 0 或 1,决定一个数字的符号,0 表示正,1 表示负


M:尾数,用小数表示,例如前面所看到的 1.234 * 10^0,1.234 就是尾数


R:基数,表示十进制数 R 就是 10,表示二进制数 R 就是 2


E:指数,用整数表示,例如前面看到的 10^-1,-1 即是指数


举例来说:

十进制的 5.0,写成二进制是 101.0 ,相当于 1.01×2^2

那么,按照上面V的格式,可以得出 S=0 , M=1.01 , E=2 。

十进制的 -5.0 ,写成二进制是 - 101.0 ,相当于 - 1.01×2^2 。

那么, S=1 , M=1.01 , E=2 。


4.IEEE浮点数标准

       直到1985年,IEEE 组织推出了浮点数标准,就是我们经常听到的 IEEE754 浮点数标准,这个标准统一了浮点数的表示形式,并提供了 2 种浮点格式:

单精度浮点数 float:32 位,符号位 S 占 1 bit,指数 E 占 8 bit,尾数 M 占 23 bit

双精度浮点数 float:64 位,符号位 S 占 1 bit,指数 E 占 11 bit,尾数 M 占 52 bit

为了使其表示的数字范围、精度最大化,浮点数标准还对指数和尾数进行了规定:

       尾数 M 的第一位总是 1(因为 1 <= M < 2),因此这个 1 可以省略不写,它是个隐藏位,这样单精度 23 位尾数可以表示了 24 位有效数字,双精度 52 位尾数可以表示 53 位有效数字


       指数 E 是个无符号整数,表示 float 时,一共占 8 bit,所以它的取值范围为 0 ~ 255。但因为指数可以是负的,所以规定在存入 E 时在它原本的值加上一个中间数 127,这样 E 的取值范围为 -127 ~ 128。表示 double 时,一共占 11 bit,存入 E 时加上中间数 1023,这样取值范围为 -1023 ~ 1024。


然后,指数E从内存中取出还可以再分成三种情况:

E不全为0或不全为1

      这时,浮点数就采用下面的规则表示,即指数E的计算值减去127(或1023),得到真实值,再将 有效数字M前加上第一位的1。

比如:0.5的二进制形式为0.1

由于规定正数部分必须为1,即将小数点右移1位,则为1.0*2^(-1)。其阶码为-1+127=126,表示为01111110,而尾数1.0去掉整数部分为0,补齐0到23位00000000000000000000000


则其二进制表示形式为:


0 01111110 00000000000000000000000

E全为0

       这时,浮点数的指数E等于1-127(或者1-1023)即为真实值, 有效数字M不再加上第一位的1,而是还原为0.xxxxxx的小数。这样做是为了表示±0,以及接近于 0的很小的数字。

E全为1

这时,如果有效数字M全为0,表示±无穷大(正负取决于符号位s)

3.解决题目

   在进行了以上部分的学习后,我们就可以完美的解决上面的程序运行结果和我们设想的不同的问题了,我们这里再一次拿到程序

int main()
{
  int n = 9;
  float* pFloat = (float*)&n;
  printf("n的值为:%d\n", n);
  printf("*pFloat的值为:%f\n", *pFloat);
  *pFloat = 9.0;
  printf("num的值为:%d\n", n);
  printf("*pFloat的值为:%f\n", *pFloat);
  return 0;
}

我们先来看第二个输出,我们定义的 n 为整形,所以分析如下

我们写出 9 的二进制码:

00000000 00000000 00000000 00001001



用IEEE浮点数标准划分后:

0  00000000  00000000000000000001001

那么此时,我们使用 %f 浮点数的方式打印


S = 0


E = -126


M = 0.00000000000000000001001


n =  (-1)^0 * 0.00000000000000000001001 * 2^(-126)


也就是非常接近0的一个很小的数字,在屏幕上输出自然就是0.000000



我们接下来看第三个输出,我们是使用 9.0 进行的赋值,所以是浮点数的赋值方法

我们写出 9.0 的二进制码

1001.0

用IEEE规定表示


(-1)^0 * 1.001 * 2^3


因为9.0是正数,所以S = 0, 这里的 3 是十进制,转化为二进制就是 10000010, M 就是在001后再补20个比特位(0)


此时数据在内存中的存储:


0 10000010 00100000000000000000000


此时我们使用 %d 的整形打印,电脑会认为我们存储的 是整形的补码,然后进行打印


01000001000100000000000000000000 —— 补码


01000001000100000000000000000000 —— 反码


01000001000100000000000000000000 —— 源码


我们将源码转换为10进制的数字

可以看到 1091567616 正好是我们第三个输出的结果


综上分析,输出如下:

      以上便是本篇文章的全部内容,在这个程序判断题目中也非常好的锻炼了我们对于理解浮点数内存存储知识的能力,希望能给您带来收获,如有不对,请多多指正。

目录
相关文章
|
9天前
|
存储 算法 关系型数据库
实时计算 Flink版产品使用合集之在Flink Stream API中,可以在任务启动时初始化一些静态的参数并将其存储在内存中吗
实时计算Flink版作为一种强大的流处理和批处理统一的计算框架,广泛应用于各种需要实时数据处理和分析的场景。实时计算Flink版通常结合SQL接口、DataStream API、以及与上下游数据源和存储系统的丰富连接器,提供了一套全面的解决方案,以应对各种实时计算需求。其低延迟、高吞吐、容错性强的特点,使其成为众多企业和组织实时数据处理首选的技术平台。以下是实时计算Flink版的一些典型使用合集。
22 4
|
10天前
|
存储 小程序 编译器
数据在内存中的存储(探索内存的秘密)
数据在内存中的存储(探索内存的秘密)
14 0
|
11天前
|
存储 监控 NoSQL
Redis处理大量数据主要依赖于其内存存储结构、高效的数据结构和算法,以及一系列的优化策略
【5月更文挑战第15天】Redis处理大量数据依赖内存存储、高效数据结构和优化策略。选择合适的数据结构、利用批量操作减少网络开销、控制批量大小、使用Redis Cluster进行分布式存储、优化内存使用及监控调优是关键。通过这些方法,Redis能有效处理大量数据并保持高性能。
34 0
|
2天前
|
存储 小程序
数据在内存中的存储
数据在内存中的存储
8 1
TU^
|
3天前
|
存储 C语言
C语言浮点数在内存中的存储
在C语言中,浮点数类型用float和double表示。float类型使用4个字节(32位),而double类型使用8个字节(64位)。浮点数表示的范围:float.h中定义
TU^
8 0
|
4天前
|
存储 算法 编译器
C语言:数据在内存中的存储`
C语言:数据在内存中的存储`
11 0
|
4天前
|
存储 小程序 编译器
C语言进阶—深度剖析数据在内存中的存储
C语言进阶—深度剖析数据在内存中的存储
|
5天前
|
存储 编译器 C语言
【C语言】深度剖析数据在内存中的存储
【C语言】深度剖析数据在内存中的存储
|
6天前
|
存储 弹性计算 监控
【阿里云弹性计算】深入阿里云ECS配置选择:CPU、内存与存储的最优搭配策略
【5月更文挑战第20天】阿里云ECS提供多种实例类型满足不同需求,如通用型、计算型、内存型等。选择CPU时,通用应用可选1-2核,计算密集型应用推荐4核以上。内存选择要考虑应用类型,内存密集型至少4GB起。存储方面,系统盘和数据盘容量依据应用和数据量决定,高性能应用可选SSD或高效云盘。结合业务特点和预算制定配置方案,并通过监控应用性能适时调整,确保资源最优利用。示例代码展示了使用阿里云CLI创建ECS实例的过程。
72 5
|
11天前
|
存储 编译器 程序员
C语言:数据在内存中的存储
C语言:数据在内存中的存储
18 2