【问题标题】:Print double without printf不使用 printf 打印双精度
【发布时间】:2021-03-12 07:43:12
【问题描述】:

我正在尝试显示一个没有 printf 或所有其他库的双精度,除了用于 malloc 的 stdlib.h。 我知道双份的库存情况,但我遇到了计算问题。

我知道 double 存储在 64 位中:

  • 1 代表标志;
  • 11 为指数;
  • 52 为值;

我使用了一些转换来获得所有这些值,但我未能获得 1.fraction(来源:https://en.wikipedia.org/wiki/Double-precision_floating-point_format),我得到了尾数,但我不知道如何正确添加这个 1。 这里有一些代码:

double              d;
unsigned long long  *double_as_int;
unsigned long long  value;

d = 0.5;
double_as_int = (unsigned long long *)&d;
value = *double_as_int & 0x001FFFFFFFFFFFFFULL;
printf("value = %llu\n", value); /* <- just for verification */

我已经知道要获得 mantisma,我只需要做 0x000FFFFFFFFFFFFULL,但我正在尝试在 1.fraction 部分中添加一个。 你们知道如何解决这部分吗?

【问题讨论】:

  • 我建议您不要将 double * 转换为 unsigned long long *。它们不仅可以具有不同的大小,而且我相信由于严格的别名,这实际上是未定义的行为。请改用memcpy
  • 指数设置隐式 1 位的值。例如,如果指数为 1027,则 1 位的值为 16(因为指数偏移了 1023)。从 MSB 开始的尾数位的权重为 8、4、2、1、0.5、0.25、0.125、0.0625 等。例如,如果指数是1027,尾数的前8位是00101100,尾数的其他位是0,那么你的数就是16 + 2 + 0.5 + 0.25 = 18.75
  • 您可以使用 union punning 将 double 转换为适当大小的 int,而不会出现未定义的行为(并且不使用 stdlib)。

标签: c printf


【解决方案1】:

我知道 double 存储在 64 位中

不一定。 “IEEE 754 双精度二进制浮点”数以 64 位存储。 “双重”可能是任何东西,也可能不是,它可能遵循 IEEE 745 标准。在假设它是C11 Annex F 之前,您应该检查__STDC_IEC_559__ 宏。

如果你想操作浮点数,你应该使用frexp和其他专门用于抽象操作浮点数表示的函数,而不需要任何*(super unsafe casts*)

double d = DBL_MIN / 2;
int exponent;
double fraction = frexp(d, &exponent);
if (fraction == 0 && exponent == 0) abort(); /*handle error*/
printf("%g = %d * 2^%d * %f\n", d, d<0?-1:1, exponent, fraction);

如何解决这部分?

1.fraction 表示像 1.01010111.. 这样的小数 in base-2。逗号后面的 digits 只是浮点数小数部分中的位,按顺序排列。以下程序(其中有许多错误)旨在以sign * 2^(exp) * [0/1].fraction(2) 的形式输出表示形式的浮点值,其中分数以基数 2 表示:

#include <stdio.h>
#include <string.h>
#include <assert.h>
#include <math.h>
#include <stdbool.h>
#include <limits.h>
#include <float.h>
#if !__STDC_IEC_559__
#error
#endif
int main() {
    double d = DBL_MIN / 2;

    typedef union {
        unsigned long long sign : 1;
        unsigned long long exp : 11;
        unsigned long long fract : 52;
    } double64u;
    double64u di;
    static_assert(sizeof(double) == sizeof(double64u), "");
    memcpy(&di, &d, sizeof(double));

    // extract **binary** digits from value into buffer
    char buffer[53] = {0};
    char *p = buffer + 52;
    unsigned long long tmp = di.fract;
    for (int i = 0; i < 52; ++i) {
        *(--p) = (tmp & 0x1) + '0';
        tmp >>= 1;
    }

    char sign = di.sign < 0 ? -1 : 1;
    
    bool normal = di.exp != 0;

    printf("%g = \n", d);
    if (normal) {
        printf("%d * 2^(%d - 1023) * 1.%s(2)\n",
            sign, di.exp, buffer);
    } else {
        printf("%d * 2^(1 - 1023) * 0.%s(2)\n",
            sign, buffer);
    }
}

在我的 x86-64 上,这个程序输出:

1.11254e-308
1 * 2^(1 - 1023) * 0.1000000000000000000000000000000000000000000000000000(2)

然后您可以将0.10.. 这是一个base 2 数字(所以我在末尾添加了(2))到一些“二进制到十进制转换器”,例如rapidtables,并且 base-2 中的 0.1 是 base-10 中的 0.5 (好吧,这个例子很简单)。所以数字是:

 1 * 2^(1 - 1023) * 0.5

然后你可以使用bc这样的无限计算器并输入数字来计算实际结果:

$ bc
scale=400
1 * 2^(1 - 1023) * 0.5
.0000000000000000000000000000000000000000000000000000000000000000000\
00000000000000000000000000000000000000000000000000000000000000000000\
00000000000000000000000000000000000000000000000000000000000000000000\
00000000000000000000000000000000000000000000000000000000000000000000\
00000000000000000000000000000000000011125369292536006915451163586662\
0203210960799023116591527666370844360221740695909792714157950

1.11254e-308相同的号码。

自己打印浮点数是一项非常艰巨的工作。我可以推荐https://www.ryanjuckett.com/printing-floating-point-numbers/ 以及介绍 Grisu3 和 Ryu 以及 Errol1 算法的论文。如需灵感,请阅读现有实现的代码:newlib vfprintf.c cvt()musl vfprintf.c fmt_fp()glibc printf_fp_ stuff

【讨论】:

  • 感谢您的回答和您的时间,我正在努力。这是一项学校作业(重新编码 printf,双重 gestion 是奖励部分)。我想最困难的部分是当我们触及 0 之间的界限部分时,就会看到!
猜你喜欢
  • 1970-01-01
  • 2019-02-09
  • 2019-05-22
  • 1970-01-01
  • 2010-11-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-10-07
相关资源
最近更新 更多