双精度浮点数无法正确减去小数

3
我正在编写一个类似于轮盘赌的C ++命令行程序。用户可以输入十进制值/数字进行投注,我使用double类型变量使此成为可能。但是,如果例如我从1美元开始,然后输掉0.23美元,然后下注0.55美元并输掉,然后下注0.07美元并再次输掉,我就不能下注0.15美元,即使该程序声称我实际上有0.15美元(您不能下注比您拥有的钱更多)。看起来程序在错误地减少资金。然而,我仍然可以下注0.149美元。值得注意的是,我使用stringstream将用户的投注输入转换为double类型值。有人能解释一下这里发生了什么吗?
以下是我的代码:
#include <iostream>
#include <sstream>
using namespace std; //Std namespace.

void string_to_number(string input, double& destination);

class Roulette {
private:
int randoms;
double money, choice, bet;
string input;
public:
int play = 0;
void start_amount() {
    cout<<"How much money do you have?: ";
    getline(cin, input);
    string_to_number(input, money);
}

void betting() {
   cout<<"How much money would you like to bet?: ";
    getline(cin, input);
    string_to_number(input, bet);

    while (bet > money) {
        cout<<"You can't bet more money than you have ("<<money<<" dollars). Please enter again: ";
        getline(cin, input);
        string_to_number(input, bet);
    }
}

void choose_number() {
    cout<<"Which number do you choose? (0-35): ";
    getline(cin, input);
    string_to_number(input, choice);
}

void random_number() {
    cout<<"The wheel is spinning..."<<endl<<flush;
    randoms = (rand())%36;
}

void scenarios() {
    cout<<"The wheel shows number "<<randoms;
    if (randoms == choice) {
        money += bet;

        cout<<", which means that you win "<<bet<<" dollars! You currently have "<<money<<" dollars."<<flush<<endl;
    }
    else {
        money -= bet;

        cout<<", which means that you lose "<<bet<<" dollars. You currently have "<<money<<" dollars."<<flush<<endl;
    }

}

};

int main(int argc, const char * argv[])
{
srand(unsigned(time(0)));
Roulette a;
a.start_amount();

while (a.play == 0) {
    a.betting();
    a.choose_number();
    a.random_number();
    a.scenarios();
}

return 0;
}


void string_to_number(string input, double& destination) {
stringstream convert(input);
if ( !(convert >> destination) )
    destination = 0;
}

我建议不要使用浮点数来存储你的钱。我还建议使用<random>头文件和std::stoi函数。 - chris
2
在进行任何与双精度数比较时,要小心randoms == choice。它们并不完全符合您的期望,因为并非所有数字都可以用双精度数表示。 - andre
1
首先想到的是,你遇到了二进制浮点数的问题。例如,值 0.1 无法在二进制中精确表示。最简单的解决方法是不要使用小数美元,而是使用整数美分。 - user180247
不详细阅读您的问题,我会把赌注放在答案是 goldberg 上。(好吧,也许这就是问题所在,答案是一个适当缩放的int)。 - BoBTFish
3
double代表一个精确的值,总是如此。如果你使用实数算术,它可能不是你预期的值,但是doubles并不是实数。另一方面,如果你知道自己在做什么,完全可以使用doubles进行某些形式的精确算术运算。 - James Kanze
请阅读:http://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html - Thomas Matthews
4个回答

4
这并不是因为程序减法有误 - 而是因为二进制小数和十进制小数不是“完全数学兼容的” - 有限的十进制小数常常是无限周期的二进制小数。
因此,对于一些十进制小数(如0.15),存在多个有效的双精度近似值,减法的结果是其中之一(A),而从字符串“0.15”转换的结果是另一个(B)。由于巧合,B比A更大。
您应该使用整数美分,而不是双精度美元,以保持精确的十进制舍入。更通用的解决方案是使用某些十进制数字类(例如这个),它使用整数实现了十进制小数算术。
一些十进制(和二进制)数字类实现了任意精度算术 - 它解决了固定点部分大于硬件支持的双类型的问题。在规定四舍五入到美分(2位小数)的应用程序中,您不需要这个。

1
@Måns - float和double确实代表数字。它们只是不能用十进制表示法表示您可以使用的完全相同的数字集合。至于pie,这些值是无理数,这意味着它们无法在任何基础上精确地表示,包括十进制。 pi不是精确的3、3.14、3.1415926或其他什么 - pie都有无限数量的数字,而且数字甚至不循环。无论您使用什么数字表示,您只能近似 pie - user180247
@Måns - 10有质因数2和5。2只有质因数2。因此,二进制(基数2)不能表示十进制(基数10)能够表示的所有数字。 1/51/10(以及像 2/5 这样的倍数)无法在二进制中精确表示,但可以在十进制中精确表示。在某种意义上,完美的基数将具有其质因数分解中每个素数的无限重复 - 如果这样的基数可以工作,它可以精确表示任何有理数。 - user180247
1
@MånsNilsson - “唯一准确表示相同数字集合的方法是使用整数?”- 不是,另一种方法是使用十进制数类(它在内部为您使用整数完成工作)。已更新并更正了我的答案。 - mas.morozov
除了十进制数类型和带有缩放的整数之外,另一种方法是使用有理数类型,如果您的语言/库支持的话。有理数的定义本质上是一个可以表示为整数比率的数字 - 一个(可能是顶重的)分数。任何可以在任何基础上有限地表示的数字都可以使用有限分数(不需要点)表示,因此结合“大整数”类型(用于分数的上下部分),几乎任何有理数都可以在计算机上表示 - 但计算会慢一些。 - user180247
1
@Steve314 使用精确有理算术进行计算可能会非常缓慢(不是一点点),因为通常分子和分母的数字大小随着乘法(除法)的数量呈指数级增长。这就是为什么精确有理算术非常少使用的原因。 - mas.morozov
显示剩余4条评论

1

1

对于我来说,我首先尝试将小数点移出,通过将两个数字都乘以100,例如0.99将变成99。然后进行减法运算后,再通过将结果除以100来恢复小数点。


0

建议在货币计算中不要使用浮点数。由于其尺寸较小,浮点数在处理大数字时很快变得不精确。

对于货币计算,您可以使用具有固定比例的整数(例如100表示一美元)。这样,您可以在需要的小数位数上获得100%的准确性。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接