C++中的双类型变量乘法会导致值不准确

double type variable multiplication in C++ results in inaccurate value

本文关键字:不准确 类型变量 C++      更新时间:2023-10-16

我在做字符串到双精度转换练习时发现了这种情况(例如stdlib.h中的atof)。我想把字符串"625"(表示双精度的小数部分)放在双精度变量0.625中。奇怪的是,当我把它作为练习的一部分时,它导致了不准确的结果0.6250000000000000011或类似的结果。然而,当我把它单独放在一个地方时,它运行得很好,就像下面的代码:

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.6250000000000000011):

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(Apple股份有限公司版本5666)(点3)。请注意,代码中的注释部分导致了问题。我请求你提出解决这个问题的办法。谢谢

事实上,第一个也不准确,如果我们以更高的精度打印结果,我们就会得到

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)

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;

10的幂(具有非负指数)可以在一段时间内精确地表示为doubles(对于指数<=22,假设64位IEEE754 doubles),并且如果分数部分不太长,分子也可以精确地表示。那么,由于必要的四舍五入,即最后的除法,只有一点会产生不精确的结果,并且结果(应该是)与精确的数学结果最接近的可表示数。(另一个不精确的点是将分数部分添加到积分部分。)

对于积分部分不太大、分数部分足够短的输入,上面的方法会产生很好的结果,但对于长分数部分,这是非常错误的。

正确解析和显示浮点数是一项复杂的业务。

您需要设置精度,以便能够输出浮点值并加倍到您选择的精度:
请尝试使用setprecision()

一点也不奇怪:double是二进制浮点表示。

这意味着它将无法正确地对某些十进制数字进行建模。

如果这对你来说没有意义,请阅读这篇文章。

如果要对小数进行建模,可以尝试使用十进制浮点表示法。

因此,如果您可以访问C++11编译器,如gcc 4.6.1,您可以使用std::decimal::decial64来准确地表示小数。如果没有,你可以使用这个库。