C语言中的十六进制浮点数表示法

3
当我在阅读C语言中浮点数的十六进制表示时,我遇到了Stephen Prata的书中的一个特殊数字"0xa.1fp10"。当我将此数字分配给float或double变量,并使用“%a”格式说明符在printf中打印时,结果是0x1.43e000p+13,与原始值不匹配。但两者都是相同的值10364(十进制)。
发生了什么?为什么输出值改变了?如何获得原始数字作为输出?

5
正如你所发现的,同一个值可以有多种可能的表达方式。C语言保留的是值本身,而非它的表达方式。 - Oliver Charlesworth
但是什么是标准表示法呢? 我如何获得原始表示法? - Ravi Raj
1
就像(十进制格式)17.3e12和1.73e13是相同的数字,但在您的情况下,差异在于十六进制,因此不太明显。 - Rudy Velthuis
我在Windows 10系统上无法使用%a转换说明符以p表示法输入浮点数。问题出在哪里? - Ravi Raj
3个回答

4

不幸的是,使用printf无法在移植性上获得与0xa.1fp10 相同的格式。C标准规定,对于一个非零的正常双精度浮点数,在小数点.之前应该有一个非零数字,并且需要尽可能多的数字来准确表示值在小数点.之后的部分。实现可以选择前面多少位放入第一个数字!

然而,C11标准有一个脚注278,它说:

二进制实现可以选择十六进制小数点字符左边的数字,以便后续数字与封装(4位)边界对齐。

这是问题所在。由于IEEE 754 double具有53位尾数; 对于正常数字,第一位为1; 其余的52位都可以被4整除,因此遵循该脚注的实现(我的机器上的Glibc似乎是其中之一),将始终输出任何以0x1.开头的有限非零浮点数!试试这个最小程序:
#include <stdio.h>

int main(void) {
    for (double i = 1; i < 1024 * 1024; i *= 2) {
        printf("%a %a %a\n", 1.0 * i, 0.7 * i, 0.67 * i);
    }
}

在我的电脑上的输出为

0x1p+0 0x1.6666666666666p-1 0x1.570a3d70a3d71p-1
0x1p+1 0x1.6666666666666p+0 0x1.570a3d70a3d71p+0
0x1p+2 0x1.6666666666666p+1 0x1.570a3d70a3d71p+1
0x1p+3 0x1.6666666666666p+2 0x1.570a3d70a3d71p+2
0x1p+4 0x1.6666666666666p+3 0x1.570a3d70a3d71p+3
0x1p+5 0x1.6666666666666p+4 0x1.570a3d70a3d71p+4
0x1p+6 0x1.6666666666666p+5 0x1.570a3d70a3d71p+5
0x1p+7 0x1.6666666666666p+6 0x1.570a3d70a3d71p+6
0x1p+8 0x1.6666666666666p+7 0x1.570a3d70a3d71p+7
0x1p+9 0x1.6666666666666p+8 0x1.570a3d70a3d71p+8
0x1p+10 0x1.6666666666666p+9 0x1.570a3d70a3d71p+9
0x1p+11 0x1.6666666666666p+10 0x1.570a3d70a3d71p+10
0x1p+12 0x1.6666666666666p+11 0x1.570a3d70a3d71p+11
0x1p+13 0x1.6666666666666p+12 0x1.570a3d70a3d71p+12
0x1p+14 0x1.6666666666666p+13 0x1.570a3d70a3d71p+13
0x1p+15 0x1.6666666666666p+14 0x1.570a3d70a3d71p+14
0x1p+16 0x1.6666666666666p+15 0x1.570a3d70a3d71p+15
0x1p+17 0x1.6666666666666p+16 0x1.570a3d70a3d71p+16
0x1p+18 0x1.6666666666666p+17 0x1.570a3d70a3d71p+17
0x1p+19 0x1.6666666666666p+18 0x1.570a3d70a3d71p+18

这个输出是有效的 - 对于每个正常数字,代码只需要输出一个0x1.,然后是所有实际尾数的十六进制值,去掉尾随的0字符并添加p+指数。
对于长双精度浮点数,x86格式具有64位尾数。由于64位恰好可以分为半字节,因此合理的实现将在“正常”数字的之前使用一个完整的半字节,其值从0x80xF(第一位始终为1),并且点后最多有15个半字节。
请使用以下内容进行测试:
#include <stdio.h>
int main(void) {
    for (long double i = 1; i < 32; i ++) {
        printf("%La\n", i);
    }
}

查看它是否符合这个期望...


在正常的正数和零之间可能存在次正常数-我的Glibc使用0x0.来表示这些双精度值,后面跟着尾数的实际十六进制数字,尾随零已经被删除,并且固定指数为-1022 - 再次强调,这种表示方法是最容易实现和计算速度最快的。

我相信星号是无意的:i < 1024*; - user694733
@user694733 有意为之,但其他1024个在哪里我不知道 :D - Antti Haapala -- Слава Україні

1
这是一个十六进制浮点格式。在0xp之间的数字(和句点)是十六进制数码,称为有效数字。在p之后的数字是十进制数码,表示要将有效数字乘以2的幂。

0xa.1fp10中,有效数字是a.1f。这代表着数字10•160 + 1•16−1 + 15•16−2,等于10 + 31/256,即2591/256。

然后p10表示将其乘以21024,因此结果为2591/256 • 1024 = 10,364。

结果只是一个数字。0xa.1fp10、10364和0x1.43ep13是代表同一个数字的三个不同数字。当您将此值存储在float或double中时,对象仅包含数字。没有记录其原始格式。当您使用%a打印它时,实现选择前导数字。因为没有原始数字的记录,除非您有一些单独的记录此信息并编写自己的软件来打印数字,否则无法使printf生成原始字符串。
浮点格式通常使用二进制基数,将十进制科学计数法正确转换为二进制浮点数的好软件很难编写(虽然这是一个已解决的问题,并有发表的论文)。使用十六进制格式而不是十进制使得很容易精确地指定作者想要的浮点数值,并且编译器也很容易解释它。十六进制格式是为此目的设计的:读写浮点数的轻松和准确性。它不是为了促进美学问题,如重现特定的缩放或规范化而设计的。
注脚: 1.前导数字是指小数点前的第一个数字。

1 当使用%a时,C标准将其缩放方式留给实现来选择,但小数点前恰好有一个数字,如果数字处于浮点格式的正常范围内,则该数字非零,并且小数点后的位数等于精度。


1
但两者在十进制中都是相同的值10364。
确实。
发生了什么?为什么输出值改变了?
为什么不应该改变呢?内存中double的表示不包含任何格式信息。正如你自己观察到的,输出表示与输入表示相同,因此值并没有改变。只是以不同的方式表示。
使用%e指令,也可以对十进制数进行类似的行为。
我怎样才能得到原始数字作为输出?
很有可能你无法让你的特定printf()实现发出程序从其输入中读取的特定表示。然而,如果该表示具有某种系统性,例如在小数点前提供单个十六进制数字的最小指数,则原则上可以编写自己的输出函数来产生该表示。
在评论中添加,
但标准表示是什么?

从C语言标准所要求的表示方式来看,实际上并不存在一种特定的表示方式。该语言只要求在小数点前面恰好有一个十六进制数字,并且如果数字被标准化且本身不为零,则该数字必须非零。这样对于大多数标准化的浮点数,就有四种可能的表示方式。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接