在C++中存储货币值的最佳方法

69

我知道使用浮点数存储货币值不合适,因为会存在舍入误差。在C++中有标准的表示货币的方法吗?

我查看了boost库但没有找到相关内容。在Java中,好像BigInteger是一种方式,但我找不到相应的C++等价物。如果有已经测试过的方案,我宁愿不自己编写货币类。


1
关于信息,使用二进制表示法或十进制表示法没有更多或更少的舍入误差(参见1/3=0.333...)。使用十进制表示法只是让您拥有与手动计算相同的舍入误差。(更容易检查/匹配结果) - Offirmo
@Offirmo:说得对。但是,如果你进行金融计算,很多错误可能源于十进制货币必须被转换成二进制货币的事实。 - Sebastian Mach
21个回答

34

不要仅将其储存为分,因为在计算税和利息时,乘法操作会很快积累误差。最起码,保留额外的两个有效数字:$12.45应该被存储为124,500。如果您将其保存在带符号的32位整数中,您将有200,000美元可用于工作(正或负)。如果需要更大的数字或更高的精度,带符号的64位整数可能会给您足够长时间内所需的所有空间。

将该值包装在类中可能会有所帮助,这样可以为您提供一个地方来创建这些价值、对它们进行运算并为其格式化显示。这也将为您提供一个集中的地方来存储所使用的货币(USD、CAD、欧元等)。


2
你是怎么得到 2,000,000 的?在有符号的32位整数中,你可以储存多达约20亿分之一美元,大约为20百万美元。将其中两位数字去掉以获得更高的精度,你就剩下约 200 千美元。 - wilhelmtell
1
一个64位整数使用两个额外精度数字可以容纳多大的值? - Assimilater
3
我看到这篇帖子相当古老,它是否仍然反映了存储货币的最佳方式?或者在c++14和/或boost中是否添加了更好的方法? - Assimilater
2
相反,存储应该是以分为单位,因为没有小于一分钱的金额。在计算时,应该注意使用适当的类型并正确地进行四舍五入。 - einpoklum
1
@einpoklum 仅需要2位小数,但投资交易通常使用4-6位小数的值。因此,存储可能需要比分提供的更高精度。 - Remy Lebeau
@RemyLebeau:我不知道那个。所以,我猜这取决于OP想要做什么。 - einpoklum

30

我曾在实际的财务系统中处理过这个问题,我可以告诉你,如果是美元,你可能需要使用至少具有6位小数精度的数字。希望由于你谈论的是货币价值,你不会偏离太远。目前有关向C++添加十进制类型的提案,但我不知道是否已经实现。

在这里使用的最佳本机C++类型是long double。

其他方法只使用int的问题在于,你必须存储的不仅仅是几分钱。通常,金融交易会乘以非整数值,这将会给你带来麻烦,因为例如将$100.25翻译成10025 * 0.000123523(例如APR)就会引起问题。最终你将进入浮点数领域,并且转换将会很费力。

在大多数简单情况下,这个问题并不会发生。我给你一个明确的例子:

如果你给出几千个货币价值,如果你将每个价值乘以一个百分比然后相加,如果你不保留足够的小数位数,则最后得到的结果将与如果你将总值乘以该百分比得到的结果不同。现在这在某些情况下可能有效,但你很快就会少几分钱。根据我的一般经验,确保保留6位小数精度(确保剩余的精度可用于整数部分)。

还要明白,如果你进行不太精确的数学计算,则存储类型不重要。如果你的数学计算是以单精度运算的,那么无论你将其存储在双精度中还是其他精度中,你的精度都将正确到最不精确的计算。


话虽如此,如果你除了简单的加减法之外没有进行其他复杂的数学计算并存储数字,则不会有问题;但是只要出现任何比这更复杂的情况,你将陷入麻烦。


2
你能详细说明一下你对整数的反对意见,或者提供一个参考吗?使用整数进行计算得出的样例结果为$0.01或1。我不明白为什么这不是正确答案。 - Jeffrey L Whitledge
看上面的例子。我可以提供更多,但在这种情况下,通常非常简单明了。我编写了财务预测软件,你不能只用整数和四舍五入。你需要存储不仅是分,还有零点几分。最终,四舍五入问题会让你吃亏。 - Orion Adrian
我编写了一些销售点软件,我的解决方案(表现为sum(discounts-per-line-item) != discount-on-order-total)是确保您始终进行所需的计算。问题空间应该指示小百分比的总和或总额的百分比。 - Jeffrey L Whitledge
2
@Jeffrey(和其他人)-除了Orion已经说的,金融系统需要能够处理非常广泛的数字范围。股票市场上的股票(尤其是外汇汇率)计算到一分钱的小数位($0.000001),而其他货币,如津巴布韦元则经历了恶性通货膨胀(http://en.wikipedia.org/wiki/Zimbabwean_dollar#Exchange_rate_history),即使使用双精度浮点数也无法处理所使用的大值。因此,使用int、long int等选项确实不是一个选择。 - jmc

24

14

最大的问题在于四舍五入!

42.50€的19% = 8.075€。根据德国的四舍五入规则,应该是8.08 €。问题是(至少在我的电脑上),8.075无法表示为double类型。即使我在调试器中将变量更改为此值,最终结果也是8.0749999...。

这就是我的四舍五入函数(以及我能想到的任何其他浮点逻辑)失败的地方,因为它会产生8.07 €。有效数字是4,所以该值向下取整。这是明显错误的,除非您尽可能避免使用浮点值,否则无法解决这个问题。

如果您将42.50€表示为整数42500000,则它将起作用。

42500000 * 19 / 100 = 8075000。现在可以应用上述四舍五入规则,得到8080000。可以轻松将其转换为货币价值以进行显示。8.08 €。

但我总是会将其封装在一个类中。


9
你说你在Boost库中找不到与货币值和运算相关的内容。但是,你可以在multiprecision/cpp_dec_float 中找到:

此类型的基数是10。因此,它可能会与基于2的类型有微妙的不同。

所以,如果你已经在使用Boost,这应该是对货币值和运算很好的支持,因为它是基于10个数字精度高达50或100位(非常高)。

看一下:

#include <iostream>
#include <iomanip>
#include <boost/multiprecision/cpp_dec_float.hpp>

int main()
{
    float bogus = 1.0 / 3.0;
    boost::multiprecision::cpp_dec_float_50 correct = 1.0 / 3.0;

    std::cout << std::setprecision(16) << std::fixed 
              << "float: " << bogus << std::endl
              << "cpp_dec_float: " << correct << std::endl;
          
    return 0;
}

输出:

浮点数:0.3333333432674408

cpp_dec_float: 0.3333333333333333

* 我并不是说float(基于2)不好,decimal(基于10)就是好的。它们只是表现不同...

** 我知道这是一个旧帖子,并且boost::multiprecision在2013年被引入,所以我想在这里做出备注。


-1 这个回答展示了在Boost多精度库中使用十进制数据类型的不正确用法。 在这个回答中,cpp_dec_float_50 correct类型将具有与double相同的精度和基础表示形式。 查看我在此处的回答:https://dev59.com/0HVC5IYBdhLWcg3w51ny#73551215,以获取更详细的解释,为什么这个回答是误导性的,并提供如何正确使用Boost多精度十进制类型的示例。 - nessus_pp

9
你可以尝试使用十进制数据类型:

https://github.com/vpiotr/decimal_for_cpp

该数据类型旨在存储货币相关的值(余额、汇率、利率等),用户可以自定义精度,最高可达19位。
这是一个仅需头文件的C++解决方案。

9

我建议您保留一个变量来表示美分数量,而不是美元。这样可以避免舍入误差。将其显示为标准的美元/美分格式应该是视图层面的问题。


1
这并没有真正解决问题,因为你经常需要做的不仅仅是将这些数字相加,然后你会遇到精度丢失的问题。将$100.25转换为10025 * 0.0745234 APR也会导致问题。 - Orion Adrian
如果我没记错的话,有一个标准规定常见操作应保留至少4位数字 - 这就是为什么COM的“货币”给了你4个。如果涉及外币,可能需要更多。 - Joe Pineda
我在回答这个问题时解释了基于精度的计算中最小精度的问题。最终,即使您以整数形式存储数字,您仍将不得不在其他某个东西中进行计算。无论那个东西是什么,都应该成为存储机制。 - Orion Adrian
@Joe:实际上,4位小数是最低要求。我在我的计算中使用了6位小数,以获得支票操作的一分钱分辨率。但是,除非您将所有数学运算都以整数形式进行,否则您将遇到问题,因为如果您进行强制转换(隐式或显式),您将最终进入浮点数领域。 - Orion Adrian

7

了解您的数据范围。

浮点数仅适用于6到7位有效数字,因此最大值约为+-9999.99,不进行四舍五入。对于大多数金融应用程序而言,它是无用的。

双精度浮点数可达到13位数字,因此:+-99,999,999,999.99,但使用大数时仍需小心。请注意,减去两个类似的结果会削弱大部分精度(有关潜在问题,请参见《数值分析》一书)。

32位整数可达到+-20亿(按分计算将降低2位小数)

64位整数可以处理任何金额,但再次请注意,在应用程序中将其转换并乘以各种比率时要小心,可能是浮点数/双精度浮点数。

关键是了解您的问题域。您需要满足哪些精度要求?您将如何显示这些值?转换将发生多少次?您需要国际化吗?在做出决策之前,请确保能够回答这些问题。


5
无论你决定使用什么类型,我建议你用“typedef”将其包装起来,这样你就可以在不同的时间更改它。

1
鉴于typedef仅引入别名并使您暴露于隐式数字转换,我建议将其封装到一个类中。 - Sebastian Mach

4

将美元和美分作为两个独立的整数存储。


为什么要踩?这是一些主要金融机构存储价格的方式。 ;( - kmiklas

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