2012-01-07 180 views
1

我發現這是在我進行字符串到雙精度轉換練習(例如stdlib.h中的atof)時發生的。我想把一串「625」(表示雙倍數的一部分)放到一個雙倍變量0.625。奇怪的是,當我把它作爲我的練習的一部分時,它導致0.62500000000000011或類似的結果不準確。然而,當我把它放在一個獨立的方式,它工作得很好,如下面的代碼:C++中的double類型可變乘法導致值不準確

int main(int argc, char **argv) { 
string str = "625"; 
double mask = 0.1; 
double frac = 0.0; 
for(int i = 0; i < static_cast<int>(str.length()); ++i) { 
    frac += (str[i] - '0')*mask; 
    mask *= 0.1; 
} 
cout << frac << endl; 

}

上面的代碼給出準確的結果(0.625)。但是,下面的代碼給出不準確的結果(0.62500000000000011):

string PrintDecimal(string input) { 
    long int_part = 0; 
    double frac_part = 0.0; 
    bool is_positive; 
    size_t found; 
    string ret_str; 
    found = input.find('-'); 
    if(found == string::npos) { 
     is_positive = true; 
    } 
    else { 
     is_positive = false; 
     input.erase(found, 1); 
    } 
    found = input.find('.'); 
    if(found == string::npos) { 
     int mask = 1; 
     char app_char; 
     for(int i = static_cast<int>(input.length()-1); i > -1; --i) { 
      int_part += (input[i] - '0')*mask; 
      mask *= 10; 
     } 
     while(int_part != 0) { 
      app_char = (int_part % 2 == 0) ? '0' : '1'; 
      ret_str.push_back(app_char); 
      int_part /= 2; 
     } 
     if(is_positive == false) { 
      ret_str.append("-"); 
     } 
     reverse(ret_str.begin(), ret_str.end()); 
    } 
    else { 
     char app_char; 
     long mask_int = 1; 
     double mask_frac = 0.1; 
     string int_part_str = input.substr(0, found); 
     //string frac_part_str = input.substr(found+1, input.length()-found-1); 
     string frac_part_str = "0."; 
     frac_part_str.append(input.substr(found+1, input.length()-found-1)); 
     for(int i = static_cast<int>(int_part_str.length()-1); i > -1; --i) { 
      int_part += (int_part_str[i] - '0')*mask_int; 
      mask_int *= 10; 
     } 
     //This converting causes 6*0.1 = 0.6000000000009 
     /* 
     for(int i = 0; i < static_cast<int>(frac_part_str.length()); ++i) { 
      frac_part += (frac_part_str[i] - '0')*mask_frac; 
      mask_frac *= 0.1; 
     } 
     */ 
     frac_part = atof(frac_part_str.c_str()); //This works well. 
     while(int_part != 0) { 
      app_char = (int_part % 2 == 0) ? '0' : '1'; 
      ret_str.push_back(app_char); 
      int_part /= 2; 
     } 
     if(is_positive == false) { 
      ret_str.append("-"); 
     } 
     reverse(ret_str.begin(), ret_str.end()); 
     ret_str.push_back('.'); 
     found = ret_str.find('.'); 
     while(frac_part != 0.0) { 
      if(ret_str.length() - found > 64) { 
       cerr << "Can't express accurately." << endl; 
       return "Error"; 
      } 
      frac_part *= 2; 
      if(frac_part >= 1.0) { 
       ret_str.push_back('1'); 
       frac_part -= 1; 
      } 
      else { 
       ret_str.push_back('0'); 
      } 
     } 

    } 
    cout << ret_str << endl; 

    return ret_str; 
} 

我使用的編譯器的版本是gcc版本4.2.1(蘋果公司建立5666)(點3)。 請注意代碼中引起問題的註釋部分。我請求你的想法來解決這個問題。謝謝!

+5

歡迎來到浮點的美妙世界。 – 2012-01-07 15:08:00

+1

對我來說看起來相當準確。它只有0.00000000000000011出來! – 2012-01-07 15:16:05

+0

我認爲幾乎每個人都被這個浮點數引向一次。如果我告訴你寫1/3沒有分數,你會寫0.3333 ...在某些時候,你需要停下來說(足夠了)。現在,作爲一個練習,做同樣的1/10(0.1),但在二進制。 – 2012-01-07 15:29:20

回答

2

嗯,其實第一個也不會被確切的說,如果我們打印結果出更加精確,我們得到

0.62500000000000011102 

,並打印中間結果以全精度

Prelude Text.FShow.RealFloat> mapM_ print $ scanl (+) (FD 0) $ zipWith (*) (iterate (*0.1) 0.1) [6,2,5] 
0.0 
0.600000000000000088817841970012523233890533447265625 
0.62000000000000010658141036401502788066864013671875 
0.62500000000000011102230246251565404236316680908203125 

爲了獲得最準確的結果,您必須採用更復雜的算法,例如將字符串解析爲有理數並將其轉換。

快速部分解決方案是解析的小數部分得到numerator/(10^k),10

double denominator = 1.0; 
uint64_t numerator = 0; 
for(i = f0; i < len; ++i) { // f0 index of first digit after decimal point 
    numerator = 10*numerator + (str[i] - '0'); 
    denominator *= 10; 
} 
double fractional_part = numerator/denominator; 

功率(非負指數)可以爲同時被表示完全一樣double s(對於指數< = 22,假設64位IEEE754 double s),並且分子也可以精確地表示,如果小數部分不太長。那麼你只有一個點,由於必要的舍入,最終的劃分而出現不精確的結果,並且結果(應該是)與精確數學結果最接近的可表示數。 (另一個不精確的部分是將小數部分添加到整數部分。)

以上將產生良好的結果輸入不太大的積分部分和足夠短的小數部分,但它會是非常錯誤的長分數部分。

正確解析和顯示浮點數是一個複雜的業務。

+0

但以0.625爲例,它不是二進制形式無限長的小數......它可以用二進制表示爲0.101。在它之後沒有理由追加這樣一個微小的尾巴。既然它可以用有限的方式表示(二進制形式),爲什麼它會變成這樣? – 2012-01-07 16:10:37

+0

這是算法的結果。由於0.1不能被精確表示,因此乘以0.1會引入近似誤差,並且它們會累加。 – 2012-01-07 16:32:48

+0

知道了!非常感謝! – 2012-01-07 16:55:12

0

您需要設置精度能夠輸出的浮動和雙打到您選擇的精度:
嘗試使用setprecision().

+0

謝謝Als提供的解決方案之一,但我仍然不明白導致問題的原因。看,atof()很好用,它有什麼樣的魔力? – 2012-01-07 15:26:11

+0

全部取決於使用的內部浮點類型以及它試圖表示的實際數量。使用不同的編譯器或甚至不同的處理器,你會得到一個稍微不同的結果。規則1從來沒有直接比較兩個浮點,他們在外行人看來不準確,他們是近似值。 – 2012-01-07 15:39:35

0

這並不奇怪:double是一個二進制浮點表示。

這意味着它將無法正確模擬一些十進制數字。

如果這對您沒有任何意義,請參閱this

如果打算對小數點進行建模,則可能需要嘗試十進制浮點表示法。

因此,如果有權訪問C++ 11編譯器(如gcc 4.6.1),則可以使用std :: decimal :: decimal64精確地表示小數。如果不是,您可以使用this lib

+0

瞭解,我沒有注意到小數點後的0.1,不能完全用二進制表示。不管怎麼說,還是要謝謝你! – 2012-01-07 16:54:56