web-dev-qa-db-de.com

Printf-Breitenbezeichner, um die Genauigkeit des Gleitkommawerts beizubehalten

Gibt es einen printf width-Bezeichner, der auf einen Fließkomma-Bezeichner angewendet werden kann, der die Ausgabe automatisch auf die erforderliche Anzahl von signifikanten Stellen formatiert, so dass beim Scannen der Zeichenfolge der ursprüngliche Fließkommawert erfasst wird?

Angenommen, ich drucke eine float mit einer Genauigkeit von 2 Dezimalstellen:

float foobar = 0.9375;
printf("%.2f", foobar);    // prints out 0.94

Wenn ich die Ausgabe 0.94 scanne, habe ich keine normkonforme Garantie, dass ich den ursprünglichen 0.9375-Gleitkommawert zurückerhalte (in diesem Beispiel werde ich dies wahrscheinlich nicht tun).

Ich möchte, dass printf dem Gleitkommawert automatisch die erforderliche Anzahl von signifikanten Stellen anzeigt, um sicherzustellen, dass er auf den ursprünglichen Wert zurückgescannt werden kann, der an printf übergeben wurde.

Ich könnte einige der Makros in float.h verwenden, um die maximale Breite abzuleiten , um an printf zu übergeben, aber es gibt bereits einen Spezifizierer, der automatisch die erforderliche Anzahl von signifikanten Ziffern - oder zumindest die maximale Breite?

77
Vilhelm Gray

Ich empfehle @Jens Gustedt hexadezimale Lösung: verwende% a.

OP möchte "mit maximaler Genauigkeit (oder zumindest bis zur höchstwertigen Dezimalstelle) drucken".

Ein einfaches Beispiel wäre ein Siebtel wie in:

#include <float.h>
int Digs = DECIMAL_Dig;
double OneSeventh = 1.0/7.0;
printf("%.*e\n", Digs, OneSeventh);
// 1.428571428571428492127e-01

Aber lasst uns tiefer graben ...

Mathematisch lautet die Antwort "0.142857 142857 142857 ...", aber wir verwenden Gleitkommazahlen mit endlicher Genauigkeit . Nehmen wir an, IEEE 754 binär mit doppelter Genauigkeit . Der OneSeventh = 1.0/7.0 ergibt also den Wert unten. Ebenfalls dargestellt sind die vorangehenden und die folgenden darstellbaren double-Gleitkommazahlen.

OneSeventh before = 0.1428571428571428 214571170656199683435261249542236328125
OneSeventh        = 0.1428571428571428 49212692681248881854116916656494140625
OneSeventh after  = 0.1428571428571428 769682682968777953647077083587646484375

Das Drucken der dezimalen Darstellung von exact einer double hat nur begrenzte Verwendung.

C hat 2 Makrofamilien in <float.h>, die uns helfen.
Der erste Satz ist die Anzahl der signifikanten - Ziffern, die in Dezimalzahlen in einer Zeichenfolge gedruckt werden sollen. Wenn Sie also die Zeichenfolge zurück scannen, erhalten Sie den ursprünglichen Gleitkommawert. Sie werden mit dem minimum -Wert der C spec und einem sample C11-Compiler angezeigt.

FLT_DECIMAL_Dig   6,  9 (float)                           (C11)
DBL_DECIMAL_Dig  10, 17 (double)                          (C11)
LDBL_DECIMAL_Dig 10, 21 (long double)                     (C11)
DECIMAL_Dig      10, 21 (widest supported floating type)  (C99)

Der zweite Satz ist die Anzahl der signifikanten -Ziffern, die eine Zeichenfolge in einen Fließkomma-Wert scannen und dann FP gedruckt werden kann, wobei die gleiche Zeichenfolgendarstellung beibehalten wird. Sie werden mit dem minimum -Wert der C spec und einem sample C11-Compiler angezeigt. Ich glaube, dass es vor C99 verfügbar war.

FLT_Dig   6, 6 (float)
DBL_Dig  10, 15 (double)
LDBL_Dig 10, 18 (long double)

Der erste Satz von Makros scheint das Ziel von OP aus signifikanten Ziffern zu erreichen. Dieses macro ist jedoch nicht immer verfügbar.

#ifdef DBL_DECIMAL_Dig
  #define OP_DBL_Digs (DBL_DECIMAL_Dig)
#else  
  #ifdef DECIMAL_Dig
    #define OP_DBL_Digs (DECIMAL_Dig)
  #else  
    #define OP_DBL_Digs (DBL_Dig + 3)
  #endif
#endif

Das "+ 3" war der entscheidende Punkt meiner vorherigen Antwort. Im Mittelpunkt stand die Kenntnis der Roundtrip-Konvertierungszeichenfolge-FP-Zeichenfolge (Set Nr. 2 verfügbare Makros C89). Wie würde man die Ziffern für die FP-Zeichenfolge ermitteln? -FP (Set Nr. 1 Makros verfügbar nach C89)? Im Allgemeinen war Add 3 das Ergebnis.

Wie viele signifikante Ziffern gedruckt werden sollen, ist nun bekannt und wird über <float.h> gesteuert. 

Um N signifikante Dezimalstellen zu drucken, können verschiedene Formate verwendet werden. 

Bei "%e" ist das Feld precision die Anzahl der Stellen nach der führenden Ziffer und dem Dezimalpunkt . - 1 ist also in Ordnung. Hinweis: Dieser -1 is not in the initialint Digs = DECIMAL_Dig; `

printf("%.*e\n", OP_DBL_Digs - 1, OneSeventh);
// 1.4285714285714285e-01

Bei "%f" ist das Feld precision die Anzahl der Stellen nach dem Dezimalpunkt . Bei einer Zahl wie OneSeventh/1000000.0 benötigt man OP_DBL_Digs + 6, um alle signifikanten Ziffern anzuzeigen.

printf("%.*f\n", OP_DBL_Digs    , OneSeventh);
// 0.14285714285714285
printf("%.*f\n", OP_DBL_Digs + 6, OneSeventh/1000000.0);
// 0.00000014285714285714285

Hinweis: Viele verwenden "%f". Das zeigt 6 Stellen nach dem Dezimalpunkt an; 6 ist die Standardeinstellung der Anzeige, nicht die Genauigkeit der Zahl.

69
chux

Die kurze Antwort, um Fließkommazahlen verlustlos zu drucken (damit sie gelesen werden können Wieder exakt auf dieselbe Zahl, außer NaN und Infinity)

  • Wenn Ihr Typ Float ist: Verwenden Sie printf("%.9g", number).
  • Wenn Ihr Typ doppelt ist, verwenden Sie printf("%.17g", number).

Verwenden Sie NICHT %f, da dies nur angibt, wie viele signifikante Stellen hinter der Dezimalstelle stehen und kleine Zahlen abgeschnitten werden. Als Referenz finden Sie die magischen Zahlen 9 und 17 in float.h, die FLT_DECIMAL_Dig und DBL_DECIMAL_Dig definiert.

51
ccxvii

Wenn Sie sich nur für das Bit (bzw. das Hex-Muster) interessieren, können Sie das %a-Format verwenden. Dies garantiert Ihnen:

Das Die Standardgenauigkeit reicht für eine exakte Darstellung des Wertes aus, wenn eine exakte Darstellung in Basis 2 vorhanden ist und ansonsten ausreichend groß ist, um Werte des Typs double zu unterscheiden.

Ich muss hinzufügen, dass dies erst seit C99 verfügbar ist.

22
Jens Gustedt

Nein, es gibt keinen solchen printf-Breitenbezeichner, um Gleitkomma mit maximaler Genauigkeit zu drucken. Lass mich erklären warum.

Die maximale Genauigkeit von float und double ist variable und hängt vom actual-Wert der float oder double ab.

Rückruf float und double werden im Format sign.exponent.mantissa gespeichert. Dies bedeutet, dass für kleine Zahlen viel mehr Bits für die gebrochene Komponente verwendet werden als für große Zahlen.

enter image description here

Zum Beispiel kann float leicht zwischen 0.0 und 0.1 unterscheiden.

float r = 0;
printf( "%.6f\n", r ) ; // 0.000000
r+=0.1 ;
printf( "%.6f\n", r ) ; // 0.100000

float hat jedoch keine Ahnung vom Unterschied zwischen 1e27 und 1e27 + 0.1.

r = 1e27;
printf( "%.6f\n", r ) ; // 999999988484154753734934528.000000
r+=0.1 ;
printf( "%.6f\n", r ) ; // still 999999988484154753734934528.000000

Dies liegt daran, dass all die Genauigkeit (die durch die Anzahl der Mantissenbits begrenzt ist) für den größten Teil der Zahl links von der Dezimalstelle verbraucht ist.

Der %.f-Modifizierer gibt lediglich an, wie viele Dezimalwerte Sie von der Gleitkommazahl aus drucken möchten, sofern formatatting gilt. Die Tatsache, dass die verfügbare Genauigkeit von von der Größe der Anzahl abhängt, hängt von Sie als Programmierer ab. printf kann/kann das nicht für Sie erledigen.

10
bobobobo

Verwenden Sie einfach die Makros von <float.h> und den Konvertierungsspezifizierer für variable Breite (".*"):

float f = 3.14159265358979323846;
printf("%.*f\n", FLT_Dig, f);
9
user529758

Ich führe ein kleines Experiment aus, um zu überprüfen, ob beim Drucken mit DBL_DECIMAL_Dig tatsächlich die binäre Darstellung der Zahl erhalten bleibt. Es stellte sich heraus, dass für die Compiler und C-Bibliotheken, die ich ausprobiert habe, DBL_DECIMAL_Dig tatsächlich die Anzahl der erforderlichen Ziffern ist, und das Drucken mit nur einer Ziffer weniger ein erhebliches Problem darstellt.

#include <float.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

union {
    short s[4];
    double d;
} u;

void
test(int digits)
{
    int i, j;
    char buff[40];
    double d2;
    int n, num_equal, bin_equal;

    srand(17);
    n = num_equal = bin_equal = 0;
    for (i = 0; i < 1000000; i++) {
        for (j = 0; j < 4; j++)
            u.s[j] = Rand();
        if (isnan(u.d))
            continue;
        n++;
        sprintf(buff, "%.*g", digits, u.d);
        sscanf(buff, "%lg", &d2);
        if (u.d == d2)
            num_equal++;
        if (memcmp(&u.d, &d2, sizeof(double)) == 0)
            bin_equal++;
    }
    printf("Tested %d values with %d digits: %d found numericaly equal, %d found binary equal\n", n, digits, num_equal, bin_equal);
}

int
main()
{
    test(DBL_DECIMAL_Dig);
    test(DBL_DECIMAL_Dig - 1);
    return 0;
}

Ich führe dies mit Microsofts C-Compiler 19.00.24215.1 und gcc Version 6.3.0 20170516 (Debian 6.3.0-18 + deb9u1) aus. Die Verwendung einer Dezimalstelle mit weniger Dezimalstellen halbiert die Anzahl der Zahlen, die exakt gleich sind. (Ich habe auch bestätigt, dass Rand() tatsächlich etwa eine Million verschiedene Zahlen produziert.) Hier sind die detaillierten Ergebnisse.

Microsoft C

 Getestete 999523-Werte mit 17 Ziffern: 999523 gefunden numerisch gleich, 999523 gefunden binär gleich 
 Getestete 999523 Werte mit 16 Ziffern: 549780 gefunden numerisch gleich, 549780 gefunden binär gleich 

GCC

 Getestete 999492-Werte mit 17 Ziffern: 999492 für numerisch gleich befunden, 999492 für binär gleich 
 Getestete 999492-Werte für 16 Ziffern: 546615 für numerisch gefunden, 546615 für binär.
0

In einem meiner Kommentare zu einer Antwort beklagte ich mich, dass ich schon seit langem einen Weg gesucht habe, alle signifikanten Stellen in einem Fließkommawert in Dezimalform zu drucken, ähnlich wie es die Frage stellt. Nun, ich setzte mich endlich hin und schrieb es. Es ist nicht ganz perfekt, und dies ist Demo-Code, der zusätzliche Informationen ausgibt, aber es funktioniert meistens für meine Tests. Bitte lassen Sie mich wissen, ob Sie (dh jemand) eine Kopie des gesamten Wrapper-Programms möchten, das es zum Testen antreibt.

static unsigned int
ilog10(uintmax_t v);

/*
 * Note:  As presented this demo code prints a whole line including information
 * about how the form was arrived with, as well as in certain cases a couple of
 * interesting details about the number, such as the number of decimal places,
 * and possibley the magnitude of the value and the number of significant
 * digits.
 */
void
print_decimal(double d)
{
        size_t sigdig;
        int dplaces;
        double flintmax;

        /*
         * If we really want to see a plain decimal presentation with all of
         * the possible significant digits of precision for a floating point
         * number, then we must calculate the correct number of decimal places
         * to show with "%.*f" as follows.
         *
         * This is in lieu of always using either full on scientific notation
         * with "%e" (where the presentation is always in decimal format so we
         * can directly print the maximum number of significant digits
         * supported by the representation, taking into acount the one digit
         * represented by by the leading digit)
         *
         *        printf("%1.*e", DBL_DECIMAL_Dig - 1, d)
         *
         * or using the built-in human-friendly formatting with "%g" (where a
         * '*' parameter is used as the number of significant digits to print
         * and so we can just print exactly the maximum number supported by the
         * representation)
         *
         *         printf("%.*g", DBL_DECIMAL_Dig, d)
         *
         *
         * N.B.:  If we want the printed result to again survive a round-trip
         * conversion to binary and back, and to be rounded to a human-friendly
         * number, then we can only print DBL_Dig significant digits (instead
         * of the larger DBL_DECIMAL_Dig digits).
         *
         * Note:  "flintmax" here refers to the largest consecutive integer
         * that can be safely stored in a floating point variable without
         * losing precision.
         */
#ifdef PRINT_ROUND_TRIP_SAFE
# ifdef DBL_Dig
        sigdig = DBL_Dig;
# else
        sigdig = ilog10(uipow(FLT_RADIX, DBL_MANT_Dig - 1));
# endif
#else
# ifdef DBL_DECIMAL_Dig
        sigdig = DBL_DECIMAL_Dig;
# else
        sigdig = (size_t) lrint(ceil(DBL_MANT_Dig * log10((double) FLT_RADIX))) + 1;
# endif
#endif
        flintmax = pow((double) FLT_RADIX, (double) DBL_MANT_Dig); /* xxx use uipow() */
        if (d == 0.0) {
                printf("z = %.*s\n", (int) sigdig + 1, "0.000000000000000000000"); /* 21 */
        } else if (fabs(d) >= 0.1 &&
                   fabs(d) <= flintmax) {
                dplaces = (int) (sigdig - (size_t) lrint(ceil(log10(ceil(fabs(d))))));
                if (dplaces < 0) {
                        /* XXX this is likely never less than -1 */
                        /*
                         * XXX the last digit is not significant!!! XXX
                         *
                         * This should also be printed with sprintf() and edited...
                         */
                        printf("R = %.0f [%d too many significant digits!!!, zero decimal places]\n", d, abs(dplaces));
                } else if (dplaces == 0) {
                        /*
                         * The decimal fraction here is not significant and
                         * should always be zero  (XXX I've never seen this)
                         */
                        printf("R = %.0f [zero decimal places]\n", d);
                } else {
                        if (fabs(d) == 1.0) {
                                /*
                                 * This is a special case where the calculation
                                 * is off by one because log10(1.0) is 0, but
                                 * we still have the leading '1' whole digit to
                                 * count as a significant digit.
                                 */
#if 0
                                printf("ceil(1.0) = %f, log10(ceil(1.0)) = %f, ceil(log10(ceil(1.0))) = %f\n",
                                       ceil(fabs(d)), log10(ceil(fabs(d))), ceil(log10(ceil(fabs(d)))));
#endif
                                dplaces--;
                        }
                        /* this is really the "useful" range of %f */
                        printf("r = %.*f [%d decimal places]\n", dplaces, d, dplaces);
                }
        } else {
                if (fabs(d) < 1.0) {
                        int lz;

                        lz = abs((int) lrint(floor(log10(fabs(d)))));
                        /* i.e. add # of leading zeros to the precision */
                        dplaces = (int) sigdig - 1 + lz;
                        printf("f = %.*f [%d decimal places]\n", dplaces, d, dplaces);
                } else {                /* d > flintmax */
                        size_t n;
                        size_t i;
                        char *df;

                        /*
                         * hmmmm...  the easy way to suppress the "invalid",
                         * i.e. non-significant digits is to do a string
                         * replacement of all dgits after the first
                         * DBL_DECIMAL_Dig to convert them to zeros, and to
                         * round the least significant digit.
                         */
                        df = malloc((size_t) 1);
                        n = (size_t) snprintf(df, (size_t) 1, "%.1f", d);
                        n++;                /* for the NUL */
                        df = realloc(df, n);
                        (void) snprintf(df, n, "%.1f", d);
                        if ((n - 2) > sigdig) {
                                /*
                                 * XXX rounding the integer part here is "hard"
                                 * -- we would have to convert the digits up to
                                 * this point back into a binary format and
                                 * round that value appropriately in order to
                                 * do it correctly.
                                 */
                                if (df[sigdig] >= '5' && df[sigdig] <= '9') {
                                        if (df[sigdig - 1] == '9') {
                                                /*
                                                 * xxx fixing this is left as
                                                 * an exercise to the reader!
                                                 */
                                                printf("F = *** failed to round integer part at the least significant digit!!! ***\n");
                                                free(df);
                                                return;
                                        } else {
                                                df[sigdig - 1]++;
                                        }
                                }
                                for (i = sigdig; df[i] != '.'; i++) {
                                        df[i] = '0';
                                }
                        } else {
                                i = n - 1; /* less the NUL */
                                if (isnan(d) || isinf(d)) {
                                        sigdig = 0; /* "nan" or "inf" */
                                }
                        }
                        printf("F = %.*s. [0 decimal places, %lu digits, %lu digits significant]\n",
                               (int) i, df, (unsigned long int) i, (unsigned long int) sigdig);
                        free(df);
                }
        }

        return;
}


static unsigned int
msb(uintmax_t v)
{
        unsigned int mb = 0;

        while (v >>= 1) { /* unroll for more speed...  (see ilog2()) */
                mb++;
        }

        return mb;
}

static unsigned int
ilog10(uintmax_t v)
{
        unsigned int r;
        static unsigned long long int const PowersOf10[] =
                { 1LLU, 10LLU, 100LLU, 1000LLU, 10000LLU, 100000LLU, 1000000LLU,
                  10000000LLU, 100000000LLU, 1000000000LLU, 10000000000LLU,
                  100000000000LLU, 1000000000000LLU, 10000000000000LLU,
                  100000000000000LLU, 1000000000000000LLU, 10000000000000000LLU,
                  100000000000000000LLU, 1000000000000000000LLU,
                  10000000000000000000LLU };

        if (!v) {
                return ~0U;
        }
        /*
         * By the relationship "log10(v) = log2(v) / log2(10)", we need to
         * multiply "log2(v)" by "1 / log2(10)", which is approximately
         * 1233/4096, or (1233, followed by a right shift of 12).
         *
         * Finally, since the result is only an approximation that may be off
         * by one, the exact value is found by subtracting "v < PowersOf10[r]"
         * from the result.
         */
        r = ((msb(v) * 1233) >> 12) + 1;

        return r - (v < PowersOf10[r]);
}
0
Greg A. Woods