如何将 64 位十六进制值转换为 c 中的双精度值?

How to convert 64 bit hex value to double in c?

我正在使用一个 gps 模块,通过它我正在获取字符串

"0x3f947ae147ae147b"

我需要将其转换为 double。预期值为 0.02.

我参考了以下网站

https://gregstoll.com/~gregstoll/floattohex/

如何在 C 中转换值?

3F947AE147AE147B16 是 IEEE-754 二进制 64(a.k.a。“双精度”)数据的编码,值为 0.0200000000000000004163336342344337026588618755340576171875。假设您的 C 实现使用 double 的格式并且具有具有相同字节序的 64 位整数,您可以通过将其字节复制到 double 并打印它们来对其进行解码(而不是转换):

#include <errno.h>
#include <limits.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>


int main(void)
{
    char *string = "0x3f947ae147ae147b";

    //  Set errno to zero before using strtoull.
    errno = 0;
    char *end;
    unsigned long long t = strtoull(string, &end, 16);

    //  Test whether stroull did not accept all characters.
    if (*end)
    {
        fprintf(stderr,
            "Error, string \"%s\", is not a proper hexadecimal numeral.\n",
            string);
        exit(EXIT_FAILURE);
    }

    //  Move the value to a 64-bit unsigned integer.
    uint64_t encoding = t;

    /*  Test whether the number is too large, either because strtoull reported
        an error or because it does not fit in a uint64_t.
    */
    if ((t == ULLONG_MAX && errno) || t != encoding)
    {
        fprintf(stderr, "Error, string \"%s\", is bigger than expected.\n",
            string);
        exit(EXIT_FAILURE);
    }

    //  Copy the bytes into a double.
    double x;
    memcpy(&x, &encoding, sizeof x);
    printf("%.9999g\n", x);
}

这应该输出“0.0200000000000000004163336342344337026588618755340576171875”。

如果您的 C 实现不支持这种格式,您可以对其进行解码:

  • 将64位分成s,e,f,其中s是前导位,e是接下来的11位,f是剩下的52位。
  • e为2047且f为零,则报告值为+∞或-∞,根据s为0或1,停止。
  • 如果 e 为 2047 且 f 不为零,则报告值为 NaN(非数字)并停止。
  • 如果 e 不为零,则将 252 添加到 f。如果 e 为零,则将其更改为一。
  • 表示值的大小为f•2−52•2e −1023,根据s是0还是1,符号为+或-。

将一串数字(如 "0x3f947ae147ae147b")转换为实际整数的常用方法是使用“strto”函数之一。由于您有 64 位,并且您对将它们视为有符号整数不感兴趣(因为您将要尝试将它们视为 double),适当的选择是 strtoull:

#include <stdlib.h>

char *str = "0x3f947ae147ae147b";
uint64_t x = strtoull(str, NULL, 16);

现在你有了整数,你可以通过

来验证
printf("%llx\n", x);

但现在的问题是,如何将这些位视为 IEEE-754 double 值,而不是整数?至少有三种方法可以提高可移植性。

(1) 使用指针。取一个指向您的整数值 x 的指针,将其更改为 double 指针,然后间接指向它,迫使编译器(尝试)将 x 的位视为它们是double:

double *dp = (double *)&x;
double d = *dp;
printf("%f\n", d);

这曾经是一种不错且简单的方法,但它不再合法,因为它运行与“strict aliasing rule”相冲突。它可能适合你,也可能不适合你。从理论上讲,这种技术也可以 运行 解决对齐问题。由于这些原因,不推荐使用此技术。

(2) 使用联合:

union u { uint64_t x; double d; } un;
un.x = strtoull(str, NULL, 16);
printf("%f\n", un.d);

对于这种技术是否 100% 严格合法,众说纷纭。我相信它在 C 中很好,但在 C++ 中可能不行。我不知道哪里的机器不能用。

(3) 使用 memcpy:

#include <string.h>

uint64_t x = strtoull(str, NULL, 16);
double d;
memcpy(&d, &x, 8);
printf("%f\n", d);

从字面上看,这是通过将 unsigned long intx 的各个字节复制到 double 变量 d 的字节中来实现的。这是 100% 可移植的(只要 xd 大小相同)。由于额外的函数调用,我曾经认为这是一种浪费,但现在它是一种普遍推荐的技术,而且我听说现代编译器足够聪明,可以识别你正在尝试做什么,并发出完美高效的代码(也就是说,与技术 (1) 或 (2) 一样有效)。

现在,另一个可移植性问题是,这一切都假设您机器上的类型 double 实际上是使用与传入的十六进制字符串表示相同的 IEEE-754 double-precision 格式实现的。现在这实际上是一个非常安全的假设,尽管 C 标准没有严格保证。如果你想特别注意类型的正确性,你可以添加行

#include <assert.h>

assert(sizeof(uint64_t) == sizeof(double));

并将 memcpy 调用(如果这是您最终使用的)更改为

memcpy(&d, &x, sizeof(double));

(但请注意,这些最后的几项更改仅防止类型 double 大小 中出现意外的 system-specific 差异,而不是其表示形式。)

还有一点。请注意,一种绝对 不会 起作用的技术是表面上显而易见的

d = (double)x;

该行将执行值 0x3f947ae147ae147b 的实际 转换。它不会只是重新解释位。如果你尝试一下,你会得到类似 4581421828931458048.000000 的答案。 那个从哪里来的?那么,0x3f947ae147ae147b十进制是4581421828931458171,typedouble可以表示的最接近的值是4581421828931458048。(为什么typedouble不能准确表示整数4581421828931458171呢?因为它是一个62位的数字类型 double 最多有 53 位精度。)