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.计算机中的浮点数
对于我们程序员来说,说到浮点数,大多想到的是float,double,long 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
#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
然后我们下滑可以找到具体的不同类型的申明和定义,其中对于float,double,long 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^ES:符号位,取值 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 正好是我们第三个输出的结果
综上分析,输出如下:
以上便是本篇文章的全部内容,希望能给您带来收获,如有不对,请多多指正。