Problem: Casten von double in long



  • Hi Leute,

    ich habe folgenden Quelltext geschrieben:

    #include <stdio.h>
    
    class TBruch {
    
    private:
      long FZaehler, FNenner;
    
    public:
      TBruch();
      TBruch(long);
      TBruch(long, long);
      TBruch(double);
    
      void Kuerze();
      void Ausgabe();
    };
    
    void main() {
      TBruch A(3.131);
      A.Kuerze();
      A.Ausgabe();
    }
    
    TBruch::TBruch() {
      FZaehler = 0;
      FNenner = 1;
    }
    
    TBruch::TBruch(long Zaehler) {
      FZaehler = Zaehler;
      FNenner = 1;
    }
    
    TBruch::TBruch(long Zaehler, long Nenner) {
      FZaehler = Zaehler;
      FNenner = Nenner;
    }
    
    TBruch::TBruch(double Bruch) {
      long i = 10;
      while (Bruch != long(Bruch)) {
        Bruch *= 10;
        i *= 10;
      }
      FZaehler = long(Bruch);
      FNenner = i;
      Kuerze();
    }
    
    void TBruch::Kuerze() {
      int i;
    
      if (FZaehler > FNenner) {
        for (i = FNenner; i > 1; i --) {
          if (((FNenner % i) == 0) && ((FZaehler % i) == 0)) {
            FNenner /= i;
            FZaehler /= i;
          }
        }
      }
      else {
        for (i = FZaehler; i > 1; i--) {
          if (((FNenner % i) == 0) && ((FZaehler % i) == 0)) {
            FNenner /= i;
            FZaehler /= i;
          }
        }
      }
    }
    
    void TBruch::Ausgabe() {
      printf("%d / %d\n", FZaehler, FNenner);
    }
    

    Dabei entsteht das Problem, dass sich die while-Schleife in der Funktion TBruch::TBruch(double Bruch) ins Nirvana verabschiedet. Woran kann das liegen, obwohl nach dem dritten Durchgang 3131.000000... mit 3131 gleich zu sein scheint, wenn man sich die Werte beim Debuggen anschaut. Das Erstaunliche dabei ist, dass es mit manchen Zahlen funktioniert und mit manchen nicht (egal, wie viele Stellen sie nach dem Komma haben)



  • Ich könnte wieder die Gebetsmühle rausholen... 🙂

    Fließkommatypen sind ungenau. Die meisten Dezimalbrüche sind nicht ohne Fehler als double oder float darstellbar, daher wird intern gerundet. Es kann also durchaus sein, dass du durch fortgesetztes Multiplizieren mit 10 nicht genau auf eine ganze Zahl kommst.

    Du könntest prüfen, ob der Betrag der Differenz zwischen dem double und dem long unter einer bestimmten Schwelle liegt.



  • Nachtrag:
    Ich gebe auch zu bedenken, dass i lange den Wertebereich eines long überschritten haben kann, bevor der double auch nur in die Nähe von eins kommt.



  • Ist wohl mehr ein C++-Problem.

    --> verschoben ins C++-Forum



  • Sorry, aber das hat uns unser Programmier-Lehrer halt so als Aufgabe gegeben (jaa, mit Fließkommazahlen. 🙂 ) Ok, ich werd versuchen, es anders zu lösen.
    Aber dass, die Zahlen so ungenau sind, dass es schon bei 4 Nachkommastellen zu Ungenauigkeiten kommt??? Was für'n Mist...



  • Fließkommazahlen sind u.U. sehr genau, nur kann es aufgrund der Darstellung im Speicher (Mantisse und Exponent) bei Rechenoperationen zu starken Ungenauigkeiten kommen, da der Exponent erst gleichgesetzt werden muß.

    Eventuell mal nach numerischer Mathematik googlen - die beschäftigt sich mit dieser Problematik.



  • Hab ne schöne Lösung gefunden:

    TBruch::TBruch(double Bruch) {
      std::ostringstream temp;
      temp.precision(std::numeric_limits<double>::digits10);
      temp << Bruch;
      std::string str = temp.str();
      std::string::size_type p = str.find(".");
      if(p == std::string::npos)
        FNenner = static_cast<long>(Bruch);
      else {
        FNenner = static_cast<long>(pow(10, static_cast<int>(str.size() - ++p)));
        FZaehler = static_cast<long>(Bruch * FNenner);
      }
      Kuerze();
    }
    

    Bei dieser Variante musst du allerdings noch einiges includen:

    #include <sstream>
    #include <limits>
    #include <cmath>
    

    Hier nimmt man in C++ btw. cstdio anstatt stdio.h

    #include <stdio.h>
    


  • Und der Titel ist irrefuehrend !!! 😃

    (Bruch != long(Bruch))

    // Umwandeln: benutzt die standard umrechnungen
    long Lx = 0;
    double dx = 12.345;
    lx = dx; // lx ist nun 12 !

    // casten: = interpretieren
    // 1. problem long = 4byte double = 8byte ->aua
    // trotzdem bekommt mans hin !
    long Lx = 0;
    double dx = 12.345;
    lx = (long)dx; // boeser boeser C cast
    lx = reinterpret_cast<long>(dx); // wird dir zumindest ne warnung ausspucken, da die Groessen ungleich ...

    Preifrage, wie gross ist lx nun ???

    Ich mags ned aussrechnen !!!

    zuweisung nehm ich, wenn ich nen typ mathemathisch korrekt umwandeln will. also die werte nach vordefinierten regeln angepasst werden. flieskomma -> integer, nachkommastellen werden abgeschnitten.

    Casts nehm ich, wenn ich werte in anderen typen verstecken will.
    nen int von groesser auf kleiner und umgekehrt ist noch ueberschaubar. bei groesser auf kleiner, wird der Anteil der ueber steht abgeschnitten 8byte -> 4byte = 0x1234567800000001 => 00000001;
    bei flieskomma zu integer und flieskommas untereinander mit unterschiedlicher groesse, musst den internen aufbau der flieskommas kennen, dann kannst errechnen, was bei rauskommt ....

    Ciao ...



  • Ich hab jetzt herausgefunden, wo genau der Fehler liegt.

    41:     while (Bruch != long(Bruch)) {
    004011C4   fld         qword ptr [ebp+8]
    004011C7   call        __ftol (004014b8)
    004011CC   mov         dword ptr [ebp-0Ch],eax
    004011CF   fild        dword ptr [ebp-0Ch]
    004011D2   fcomp       qword ptr [ebp+8]
    004011D5   fnstsw      ax
    004011D7   test        ah,40h
    004011DA   jne         TBruch::TBruch+53h (004011f3)
    42:       Bruch *= 10;
    004011DC   fld         qword ptr [ebp+8]
    004011DF   fmul        qword ptr [__real@8@4002a000000000000000 (00426020)]
    004011E5   fstp        qword ptr [ebp+8]
    43:       i *= 10;
    004011E8   mov         eax,dword ptr [ebp-8]
    004011EB   imul        eax,eax,0Ah
    004011EE   mov         dword ptr [ebp-8],eax
    44:     }
    004011F1   jmp         TBruch::TBruch+24h (004011c4)
    45:     FZaehler = long(Bruch);
    004011F3   fld         qword ptr [ebp+8]
    004011F6   call        __ftol (004014b8)
    004011FB   mov         ecx,dword ptr [ebp-4]
    004011FE   mov         dword ptr [ecx],eax
    46:     FNenner = i;
    00401200   mov         edx,dword ptr [ebp-4]
    00401203   mov         eax,dword ptr [ebp-8]
    00401206   mov         dword ptr [edx+4],eax
    47:     Kuerze();
    00401209   mov         ecx,dword ptr [ebp-4]
    0040120C   call        @ILT+0(TBruch::Kuerze) (00401005)
    48:   }
    

    Der Befehl fld lädt hier den Wert irgendwie falsch gerundet (?) ins Float-Register. (das meinte wohl Cocaine...)

    Also hab ich die Abfrage etwas geändert:

    TBruch::TBruch(double Bruch) { 
      long i = 1; 
      while ((Bruch * i) != long(Bruch * i)) { 
        i *= 10; 
      } 
      FZaehler = long(Bruch * i); 
      FNenner = i; 
      Kuerze(); 
    }
    

    Dadurch werden die Werte ja direkt innerhalb der Float-Register verglichen.



  • Das hat nichts mir Float-Registern zu tun.

    Dein neuer Code läuft besser, weil du nicht mehr mit dem gerundeten Ergebnis aus dem letzten Schleifendurchlauf weiterarbeitest, sondern den Wert jedesmal neu berechnest und sich der Rundungsfehler so nicht verstärken kann.

    Du hast damit den Fehler nicht behoben, er wird nur seltener auftreten.

    Verwende mal 2 statt 10. 😉



  • MFK schrieb:

    Das hat nichts mir Float-Registern zu tun.

    Und wieso steht dann beim ersten Durchlauf +3.13099999999999978e+0000 im Float-Register!? Die Zahl, die ich benutze ist doch 3.131!!!

    Wieso sollte ich mit 2 multiplizieren, wenn ich die Kommastelle verschieben will? 🙄 (Ja, nur zum Testen, ich versteh schon...)



  • Stevie schrieb:

    MFK schrieb:

    Das hat nichts mir Float-Registern zu tun.

    Und wieso steht dann beim ersten Durchlauf +3.13099999999999978e+0000 im Float-Register!?

    Weil genau diese Zahl ins FPU-Register geladen wird. Die steht vorher schon so im Speicher (bei [ebp+8]). Der Grund ist schlicht und einfach, dass im Binärsystem nur Brüche, die sich aus Zweierpotenzen zusammensetzen, nicht-periodisch sind (1/2, 1/4, 1/8 usw.).



  • Soll das heißen, dass garnicht erst 3.131 gespeichert wird, sondern +3.13099999999999978e+0000? Dann würde ein Vergleich von 3.131 und +3.13099999999999978e+0000 also true ergeben, oder wie?

    Hab's ausprobiert...

    (+3.13099999999999978e+0000 == 3.131)
    

    ergibt true!!! 😮



  • Jo, viele "harmlose" Zahlen haben eine periodische Binärdarstellung, z.b. 0,1 = (binär)0,00011001100110011...



  • Boah, dass das sooo übel ist, hätte ich ja nie vermutet! Wie wird das denn gemacht, wenn es wirklich wichtig ist, dass die Zahlen genau sind? Da muss man doch dann andere Datentypen benutzen, oder?



  • Absolute Genauigkeit gibt es nicht, am nächsten kommst du dem mit symbolischer Mathematik ala Maple, höllisch aufwendig, dafür ist sqrt(x^2) aber exakt x. Kommst du mit den Grundrechenarten aus, bieten sich rationale Zahlen mit Bignum-Arithmetik an, d.h. Zähler und Nenner werden getrennt geführt und können jeweils beliebig groß sein. Einige Sprachen haben sowas eingebaut (Common Lisp z.B.). Brauchst du Genauigkeit auf einige Stellen von Dezimalzahlen, z.b. für Finanzberechnungen, dann gibt es in einigen Sprachen einen speziellen decimal-Typ dafür. Ich glaub Cobol und C# (bzw. .net) haben sowas.
    Die ganzen Sachen kann man sich natürlich auch selbst als Klassen bauen, wenn man Lust hat 😉


Log in to reply