IEEE浮点数表示法(转)
月初还在上班的时候,就天天盼望着过年放长假.然而终于熬到了过年,却发现自己的12天的长假将在碌碌无为中度过.不少朋友都已经启程前往远方,心里直打鼓啊!最近一段时间板块的人气有所下降,板块里的违规现象也有所减少.像我这样的游子最是感到遗憾的是无法团聚.也只能向一直支持着我们这个技术社区的朋友们致以节日问候,祝愿新的一年里收入节节高升,晋升有望,身体健康,家庭幸福!
近期在版区里有关于C++语言中浮点数精度问题的讨论逐渐增多,在此背景下我想对这一技术细节进行详细解析以便大家更好地理解这一技术细节。为了使讲解更加清晰,在这里我会以float类型为例展开说明双倍精度类型的基本原理。值得注意的是虽然双倍精度类型与float类型在存储架构上存在差异但两者的区别仅在于内存占用不同因此双倍精度类型能够提供更高的数值精度这一特性使得它在科学计算领域具有更为广泛的应用前景。
软件与硬件之间的兼容性是一个重要考量因素为此我必须提醒读者只有通过对二进制十进制以及十六进制数据之间转换关系有着深刻的理解才能真正掌握相关知识体系同时还需要熟练掌握如何利用VC.net工具构建基础控制台应用程序这样才能确保自己能够全面深入地阅读并理解本文内容。
好了,请大家跟我一起开始吧。
众所周知,在内存中所有数据都是以二进制形式存储的(每位只能是 )每一位单独的一个称为一位)。而在x86CPU架构中每个字节由8位组成。例如,在使用short整数类型时(占两个字节),数值如4497会被表示为十六进制形式:2C57H。这种表示方法遵循大端内存布局的规定这意味着最高有效位位于最低有效位之前即数值高位位于低位之前这与计算机系统中的常规处理方式一致因此我们通常会将其表示为2C57H的形式而非相反的方式。
浮点数是如何存储数据的具体方式是什么?目前所有遵循C/C++编程语言标准的编译器均采用IEEE(国际电子电器工程师协会)所制定的标准来进行浮点运算。这种数据结构采用了类似科学计数法的方式,并通过符号位、指数部分和尾数值这三个组成部分进行编码。底数值固定为2,在计算时会将其转换为尾值乘以2的指数次方再结合符号得到最终结果。我们接下来详细探讨float类型的存储规范:
float 表示为32-bit格式占用了4个字节。从最高到最低依次排列了第31号至第0号二进制数字。其中第31号是符号字段:1代表数值为负数, 0则代表正数。接着从第30号至第23号这8个二进制数字作为指数部分。剩余从第22号至第0号这共23个二进制数字构成了尾数部分。按照每连续8个二进制数字分成一组,则总共有四组:A组至D组四个区间段。其中每个小组都是一个完整的字节,并且在内存中以逆序形式进行了存储:DCBA顺序排列
为了避免处理逆序存储的问题带来困扰,在讲解过程中我会特别注意让内容条理清晰。因此我打算按常规顺序进行讲解,并在此后将它们倒置处理。
现在让我们遵循IEEE浮点数表示法的标准,并详细阐述如何将float型浮点数具体转化为十六进制代码。对于不带小数部分的浮点数值而言,在转换过程中我们只需关注整数值部分:即'1 1110 0'这一串二进制数字可以等价地表示为'1 7895'的形式。通过连续向左移动小数点的方式操作——直到最高有效位仅剩一位(即最左边的那个'1')——我们便能够得到最终形式:即尾部为' .7895 '而指数值则相应地增加相应的数量级。经过这样的计算后我们就可以清晰地看到所需的尾部数据与指数部分已经确定完毕。值得注意的是最高有效位始终为'一'这一点是显而易见的——因为类似的情况如果出现在日常生活中(比如购买鸡蛋的数量)同样会遇到同样的逻辑限制(当然这里指的就是不可能出现像零头这样的情况)。因此这一位我们可以放心地忽略不计因为它并不会影响到后续的实际计算过程。这样一来我们就得到了完整的尾部二进制数据并将其扩展至23位长度——通过在末尾添加足够的零来进行填充工作从而实现了完整的十六进制编码过程
再来看看指数吧!它占用了8位来存储数值信息。这些位中的一部分用于编码数值本身的大小(即尾码),另一部分则用于编码数值的位置(即指数)。具体来说,这8位中有一位用于编码符号信息(即正负号),剩下的7位则用于编码尾码和指数相关的参数。
在上述基础上,在下面我将举一个包含小数点的例子来阐述为何会产生精度问题。按照IEEE标准将float型数值123.456f转换为十六进制编码时,在具体实现中会遇到一些挑战性的问题。其中一部分涉及数值拆分的问题:即需要分别处理整数值部分和分数部分(即十进制纯小数值)。具体来说,在处理分数值时会遇到较为复杂的过程:例如如何将十进制纯分数如0.57826分解并转化为对应的二进制表示方法?由于每位数字的位置决定了其权重(如十分位对应权重为1/101=0.1;百分位对应权重为1/102=0.01;千分位对应权重为1/10^3=0.001等),因此我们可以将其展开为如下形式:n = S₁ * ( 1 / ( 2 ^ 3 ) ) + S₂ * ( 1 / ( 2 ^ 4 ) ) + …… + Sₙ * ( 1 / ( b ^ n )) ,其中每一位S₁,S₂,S₃,…,Sₙ依次代表相应的小数值位(例如对于十进制纯分数而言各位依次是5、7、8、2、6等)。这个展开式也可以推广至任意基数b下的纯分数表示方法:n = Σ_{i=1}^{n} [ Si * ( -base^{-i} ) ] 。
真的已经很久没有接触二进制运算啦!不过为了广大编程爱好者的需求继续深入学习也是值得的!我们现在来仔细分析一下二进制纯小数的概念吧!例如像二进制纯小数部分为0.1001_{\text{base } 2}这样的数值就容易理解很多啦!它的权值序列其实就是\frac{1}{2^{\text{i}}}的形式即\frac{1}{2^{\text{i}}}从i=1开始递增依次得到\frac{1}{2}(即0.5)、\frac{1}{4}(即0.25)、\frac{1}{8}(即0.125)以及\frac{1}{\text{i}^{4}}(即0.6 \times 396978663377977899...)。通过将S序列中的元素乘以相应的权值并累加就能得到原始数值啦!现在测试一下你的知识掌握情况:将十进制纯小数0.45_{\text{base } 1_{\text{base } 4}}转换为相应形式应该如何计算呢?赶紧动手算一算吧!不要提前看答案哦!这对你深入理解会有很大帮助呢!
你一定迫不及待想了解答案了吧?因为这个计算根本无法完成啊。让我们看看步骤:计算从1除以2的一次方开始(为了简化起见,在下面我们将使用指数来表示每一位)。由于0.456小于位权值0.5的原因是这一位为0;接下来第二位中大于等于0.25,则该位设为1,并将差值减去这一部分继续计算;第三位同样地进行比较与调整;第四位继续按照这种方法处理;第五位则由于数值不足而被设为零……即使在尾数最大长度限制下依然无法精确得出结果!这就是著名的浮点数精度问题的根本原因所在。在这里我并非要深入讲解数值计算方法以提高精度问题(这实在太复杂了),我只是想通过简单明了的方式向大家介绍浮点数的表示方法就足够了。
我们继续
某BC问
大家齐声数着
接下来介绍如何将纯小数转换为十六进制形式。以纯小数为例,例如0.0456,我们需要对其进行归一化处理,将其表示为1.xxxx乘以2的n次方的形式,而计算得到对应的指数n可以通过以下公式确定:
n = \text{int}(1 + \log_2 X)
我们能够将这个数值表示为= ( \text{浮点数值} ) \times ( \text{基数} )^{\text{指数}}的形式;即= ( \text{尾数} ) \times ( \text{基数} )^{\text{指数}}。转换完成后按照上一实例中的步骤处理:
首先将二进制数据\text{输入值}取其有效数值;
接着去除最高位;
然后计算\text{-指数} + \text{偏置常数};
最后按照上述流程处理得到最终结果编码如下:
\boxed{\text{结果编码}}
另外不得不提到的一点是0.0f对应的十六进制是00 00 00 00,记住就可以了。
在转换方式上类似于float类型时所采用的方法。
其主要区别在于占用内存空间大小的不同——双精度型占据64个二进制
其中包含一个额外的指数域。
双精度型会将该指数域的偏移量设定为
其相对单精度型来说增加了较多的空间以存储更大的数值范围和更高的精度。
不过需要注意的是,在单精度浮点数中,
这种额外的信息被缩减为了仅仅8个二进制位置,
并以较小幅度提升数值范围以及降低存储效率。
