6

我是 C 的新手,从书本/网上学习。我正在尝试编写一个函数,我可以将任何传递double给并返回一个int以在printf("%.*lf" ...语句中使用,这样int返回的既不会降低精度也不会产生尾随零。

我有一个工作函数,但它非常大,因为它是为了可读性而编写的,并且都被评论了。

总结一下这个函数,我计算了需要多少除以 10 才能得到doublerange 10 > d >= 0,只取小数部分并将其放入string带有 n 位小数的 a 中n = 15 - number_of_digits_left_of_decimal(我读到该类型double只能跟踪 15 位数字),从右到左检查string尾随零并保持计数,最后,返回一个int表示小数点右侧非零位数的数字。

有没有更简单的方法?谢谢。

int get_number_of_digits_after_decimal(double d)
{
  int i = 0;      /* sometimes you need an int */
  int pl = 0;     /* precision left = 15 - sigfigs */
  int sigfigs = 1; /* the number of digits in d */
  char line[20];  /* used to find last non-zero digit right of the decimal place */
  double temp;    /* a copy of d used for destructive calculations */

  /* find digits to right of decimal */
  temp = d;
  while(sigfigs < 15)
  {
    if(temp < 0)
      temp *= -1;
    if(temp < 10)
      break;
    temp /= 10;
    ++sigfigs;
  }
  /* at this point 10 > temp >= 0
  * decrement temp unitl 1 > temp >=0 */
  while(temp > 1)
  {
    --temp;
  }
  if(temp == 0)
    return(0);
  pl = 15 - sigfigs;   /* if n digits left of decimal, 15-n to right */
  switch(pl)
  {
  case 14:
    sprintf(line, "%.14lf", d);
    break;
  case 13:
    sprintf(line, "%.13lf", d);
    break;
  case 12:
    sprintf(line, "%.12lf", d);
    break;
  case 11:
    sprintf(line, "%.11lf", d);
    break;
  case 10:
    sprintf(line, "%.10lf", d);
    break;
  case 9:
    sprintf(line, "%.9f", d);
    break;
  case 8:
    sprintf(line, "%.8lf", d);
    break;
  case 7:
    sprintf(line, "%.7lf", d);
    break;
  case 6:
    sprintf(line, "%.6lf", d);
    break;
  case 5:
    sprintf(line, "%.5lf", d);
    break;
  case 4:
    sprintf(line, "%.4lf", d);
    break;
  case 3:
    sprintf(line, "%.3lf", d);
    break;
  case 2:
    sprintf(line, "%.2lf", d);
    break;
  case 1:
    sprintf(line, "%.1lf", d);
    break;
  case 0:
    return(0);
    break;
  }
  i = (strlen(line) - 1); /* last meaningful digit char */
  while(1) /* start at end of string, move left checking for first non-zero */
  {
    if(line[i] == '0') /* if 0 at end */
    {
      --i;
      --pl;
    }
    else
    {
      break;
    }
  }
  return(pl);
}
4

3 回答 3

9

可能没有更简单的方法了。这是一个相当复杂的问题。

由于以下几个原因,您的代码无法正确解决:

  • 浮点运算的大多数实际实现不是十进制的,它们是二进制的。因此,当您将浮点数乘以 10 或除以 10 时,可能会丢失精度(这取决于数字)。
  • 即使标准64-bit IEEE-754浮点格式53为尾数保留位,相当于floor(log10(2 ^ 53))=15十进制数字,但这种格式的有效数字在精确打印时可能需要在小数部分中最多包含一些1080十进制数字,这就是您的样子询问。

解决此问题的一种方法是使用%a格式类型说明符 in snprintf(),它将使用尾数的十六进制数字打印浮点值,并且从 1999 年开始的 C 标准保证,如果浮点格式,这将打印所有有效数字是 radix-2(AKA base-2 或简单的二进制)。所以,有了这个,你可以获得数字尾数的所有二进制数字。从这里您将能够计算出小数部分中有多少个十进制数字。

现在,请注意:

1.00000 = 2 +0 = 1.00000(二进制)
0.50000 = 2 -1 = 0.10000
0.25000 = 2 -2 = 0.01000
0.12500 = 2 -3 = 0.00100 0.06250
= 2 -4 = 0.00010
0.03125 = 1.0 = 2 -0

等等。

您可以在这里清楚地看到,二进制表示i中该点右侧的第 -th 位置的二进制数字也会在十进制表示中该点右侧的第 -th 位置产生最后一个非零十进制数字i

因此,如果您知道二进制浮点数中最低有效的非零位在哪里,您就可以计算出精确打印数字的小数部分需要多少十进制数字。

这就是我的程序正在做的事情。

代码:

// file: PrintFullFraction.c
//
// compile with gcc 4.6.2 or better:
//   gcc -Wall -Wextra -std=c99 -O2 PrintFullFraction.c -o PrintFullFraction.exe
#include <limits.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <math.h>
#include <float.h>
#include <assert.h>

#if FLT_RADIX != 2
#error currently supported only FLT_RADIX = 2
#endif

int FractionalDigits(double d)
{
  char buf[
           1 + // sign, '-' or '+'
           (sizeof(d) * CHAR_BIT + 3) / 4 + // mantissa hex digits max
           1 + // decimal point, '.'
           1 + // mantissa-exponent separator, 'p'
           1 + // mantissa sign, '-' or '+'
           (sizeof(d) * CHAR_BIT + 2) / 3 + // exponent decimal digits max
           1 // string terminator, '\0'
          ];
  int n;
  char *pp, *p;
  int e, lsbFound, lsbPos;

  // convert d into "+/- 0x h.hhhh p +/- ddd" representation and check for errors
  if ((n = snprintf(buf, sizeof(buf), "%+a", d)) < 0 ||
      (unsigned)n >= sizeof(buf))
    return -1;

//printf("{%s}", buf);

  // make sure the conversion didn't produce something like "nan" or "inf"
  // instead of "+/- 0x h.hhhh p +/- ddd"
  if (strstr(buf, "0x") != buf + 1 ||
      (pp = strchr(buf, 'p')) == NULL)
    return 0;

  // extract the base-2 exponent manually, checking for overflows
  e = 0;
  p = pp + 1 + (pp[1] == '-' || pp[1] == '+'); // skip the exponent sign at first
  for (; *p != '\0'; p++)
  {
    if (e > INT_MAX / 10)
      return -2;
    e *= 10;
    if (e > INT_MAX - (*p - '0'))
      return -2;
    e += *p - '0';
  }
  if (pp[1] == '-') // apply the sign to the exponent
    e = -e;

//printf("[%s|%d]", buf, e);

  // find the position of the least significant non-zero bit
  lsbFound = lsbPos = 0;
  for (p = pp - 1; *p != 'x'; p--)
  {
    if (*p == '.')
      continue;
    if (!lsbFound)
    {
      int hdigit = (*p >= 'a') ? (*p - 'a' + 10) : (*p - '0'); // assuming ASCII chars
      if (hdigit)
      {
        static const int lsbPosInNibble[16] = { 0,4,3,4,  2,4,3,4, 1,4,3,4, 2,4,3,4 };
        lsbFound = 1;
        lsbPos = -lsbPosInNibble[hdigit];
      }
    }
    else
    {
      lsbPos -= 4;
    }
  }
  lsbPos += 4;

  if (!lsbFound)
    return 0; // d is 0 (integer)

  // adjust the least significant non-zero bit position
  // by the base-2 exponent (just add them), checking
  // for overflows

  if (lsbPos >= 0 && e >= 0)
    return 0; // lsbPos + e >= 0, d is integer

  if (lsbPos < 0 && e < 0)
    if (lsbPos < INT_MIN - e)
      return -2; // d isn't integer and needs too many fractional digits

  if ((lsbPos += e) >= 0)
    return 0; // d is integer

  if (lsbPos == INT_MIN && -INT_MAX != INT_MIN)
    return -2; // d isn't integer and needs too many fractional digits

  return -lsbPos;
}

const double testData[] =
{
  0,
  1, // 2 ^ 0
  0.5, // 2 ^ -1
  0.25, // 2 ^ -2
  0.125,
  0.0625, // ...
  0.03125,
  0.015625,
  0.0078125, // 2 ^ -7
  1.0/256, // 2 ^ -8
  1.0/256/256, // 2 ^ -16
  1.0/256/256/256, // 2 ^ -24
  1.0/256/256/256/256, // 2 ^ -32
  1.0/256/256/256/256/256/256/256/256, // 2 ^ -64
  3.14159265358979323846264338327950288419716939937510582097494459,
  0.1,
  INFINITY,
#ifdef NAN
  NAN,
#endif
  DBL_MIN
};

int main(void)
{
  unsigned i;
  for (i = 0; i < sizeof(testData) / sizeof(testData[0]); i++)
  {
    int digits = FractionalDigits(testData[i]);
    assert(digits >= 0);
    printf("%f %e %.*f\n", testData[i], testData[i], digits, testData[i]);
  }
  return 0;
}

输出(ideone):

0.000000 0.000000e+00 0
1.000000 1.000000e+00 1
0.500000 5.000000e-01 0.5
0.250000 2.500000e-01 0.25
0.125000 1.250000e-01 0.125
0.062500 6.250000e-02 0.0625
0.031250 3.125000e-02 0.03125
0.015625 1.562500e-02 0.015625
0.007812 7.812500e-03 0.0078125
0.003906 3.906250e-03 0.00390625
0.000015 1.525879e-05 0.0000152587890625
0.000000 5.960464e-08 0.000000059604644775390625
0.000000 2.328306e-10 0.00000000023283064365386962890625
0.000000 5.421011e-20 0.0000000000000000000542101086242752217003726400434970855712890625
3.141593 3.141593e+00 3.141592653589793115997963468544185161590576171875
0.100000 1.000000e-01 0.1000000000000000055511151231257827021181583404541015625
inf inf inf
nan nan nan
0.000000 2.225074e-308 0.00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002225073858507201383090232717332404064219215980462331830553327416887204434813918195854283159012511020564067339731035811005152434161553460108856012385377718821130777993532002330479610147442583636071921565046942503734208375250806650616658158948720491179968591639648500635908770118304874799780887753749949451580451605050915399856582470818645113537935804992115981085766051992433352114352390148795699609591288891602992641511063466313393663477586513029371762047325631781485664350872122828637642044846811407613911477062801689853244110024161447421618567166150540154285084716752901903161322778896729707373123334086988983175067838846926092773977972858659654941091369095406136467568702398678315290680984617210924625396728515625

您可以看到,π并且0.1只有15十进制数字才是正确的,其余数字显示了数字真正四舍五入的结果,因为这些数字不能以二进制浮点格式精确表示。

您还可以看到DBL_MIN,最小的正归一化double1022在小数部分有数字,其中有715有效数字。

此解决方案可能存在的问题:

  • 您的编译器的printf()函数不支持%a或无法正确打印精度要求的所有数字(这很有可能)。
  • 您的计算机使用非二进制浮点格式(这种情况极为罕见)。
于 2013-03-04T08:11:03.563 回答
5

我注意到的第一件事是您正在除以temp10这会导致精度下降。

不是要关闭您或阻止您再次尝试,而是要正确执行此操作比您所展示的要复杂得多。

Guy L. Steele 和 Jon L. White 写了一篇名为“如何准确地打印浮点数”的论文,详细介绍了一些陷阱,并提出了一种打印浮点数的工作算法。这是一个很好的阅读。

于 2013-03-04T05:14:17.997 回答
1

没有简单的方法。

二进制浮点类型和十进制表示之间的严格无损或全精度转换(对于浮点到字符串的转换,“ dtoa”),在幼稚的情况下不容易完成。事实上,即使是经典的dtoa实现也可能足够复杂。有关详细信息,请参阅此问题

<charconv>对于研究和工业实现(例如 ISO C++'s )来说,这仍然是一个活跃的领域。在过去十年中开发了各种新算法。仅举几例,有 GrisuExact、Ryu、Schubfach 和 Dragonbox。这些算法旨在尽可能快地处理“正常”情况,并在往返转换中保证全精度和无损属性(对于 IEEE-754 binary64 而言,这通常需要 768 字节的内部缓冲区double,并且至少需要 float96+int128 功能,如果不是任意精度的算术运算),以可移植性为代价:它们依赖于浮点类型的内部表示,而 C 和 C++ 等语言无法保证这一点。

这些算法的一些实现在此处收集(和基准测试) ,并带有指向其他资源的链接。您可以看到每个实现都有数千行 C/C++ 代码。其中一些可以保证消除尾随零,但结果可能不是您想要的。(因为它们是完全精确的,而不是四舍五入,所以可能会有类似的情况...00000000001。)

另请注意,一旦您需要输出的具体格式和/或往返无损可转换保证,由于printf不同的原因,该系列通常不是完全符合代码的解决方案:它依赖于语言环境,并且具体行为在C/C++ 是实现定义的。因此,除非您还提供该语言的实现,否则您无法保证其符合 ISO C/ISO C++。

我搜索这个问题是因为我仍在试图找出一个完全可移植的解决方案(即使效率不高,但至少比从sprintf.dtoa

于 2021-11-27T20:45:42.543 回答