提问人:Valera Dubrava 提问时间:7/7/2022 最后编辑:Valera Dubrava 更新时间:10/23/2022 访问量:330
STD 中的浮点数到字符串转换实现
float number to string converting implementation in STD
问:
我遇到了一个奇怪的问题。请看这个简单的代码:
int main(int argc, char **argv) {
char buf[1000];
snprintf_l(buf, sizeof(buf), _LIBCPP_GET_C_LOCALE, "%.17f", 0.123e30f);
std::cout << "WTF?: " << buf << std::endl;
}
输出看起来是有线的:
123000004117574256822262431744.00000000000000000
我的问题是它是如何实现的?有人可以给我看原始代码吗?我没有找到它。或者也许这对我来说太复杂了。
我试图用 Java 代码重新实现相同的转换双倍到字符串,但失败了。即使我试图分别获得指数和分数部分并在循环中总结分数,我也总是得到零而不是这些数字“......822262431744".当我尝试在 23 位(对于浮点数)之后继续汇总分数时,我遇到了另一个问题 - 我需要收集多少个分数?为什么原始代码停止在左侧,直到刻度结束才继续? 所以,我真的不明白基本逻辑,它是如何实现的。我试图定义非常大的数字(例如0.123e127f)。它以十进制格式生成大量数字。该数字的精度比浮点数高得多。看起来这是一个问题,因为字符串表示包含浮点数不能包含的内容。
答:
请阅读文档:
printf, fprintf, sprintf, snprintf, printf_s, fprintf_s, sprintf_s, snprintf_s - cppreference.com
格式字符串由普通的多字节字符(除外)和转换规范组成,这些字符原封不动地复制到输出流中。每个转换规范都具有以下格式:
%
- 介绍性人物
%
- ...
- (可选)后跟整数或 ,或者两者都不指定转换精度。在使用 when 的情况下,精度由 int 类型的附加参数指定,该参数出现在要转换的参数之前,但如果提供了最小字段宽度,则出现在提供最小字段宽度的参数之后。如果此参数的值为负数,则忽略该参数。如果既不使用数字,也不使用,则精度为零。有关精度的确切影响,请参见下表。
.
*
*
*
....
转换说明符 解释 预期参数类型 f
F
将浮点数转换为样式为 [-]ddd.ddd 的十进制表示法。精度指定小数点字符后显示的确切位数。默认精度为 6。在替代实现中,即使后面没有数字,也会写入小数点字符。有关无穷大和非数字转换样式,请参阅注释。 双
因此,对于您强制形式(无指数),并且您被迫在小数点分隔符后显示 17 位数字。如此大的价值,打印结果看起来很奇怪。f
ddd.ddd
.17
您正在使用的 C++ 实现使用 IEEE-754 binary32 格式。在这种格式中,0.123•1030 的壁橱可表示值为 123,000,004,117,574,256,822,262,431,744,以 binary32 格式表示为 +13,023,132•273。因此,在源代码中产生数字 123,000,004,117,574,256,822,262,431,744。(因为这个数字表示为 +13,023,132•2 73,我们知道它的值正是 123,000,004,117,574,256,822,262,431,744,即使数字“123000004117574256822262431744”没有直接存储。float
0.123e30f
然后,当您使用 格式化它时,C++ 实现会忠实地打印确切的值,从而生成“123000004117574256822262431744.000000000000000000000”。C++ 标准不要求这种准确性,并且某些 C++ 实现不会完全执行转换。%.17f
Java 规范也不要求对浮点值进行精确的格式化,至少在某些格式化操作中是这样。(我在这里是凭记忆和一些假设;我手头没有引文。它允许,甚至可能要求,只产生一定数量的正确数字,之后如果需要相对于小数点或请求的格式进行定位,则使用零。
该数字的精度比浮点数高得多。
对于以格式表示的任何值,该值都具有无限的精度。数字 +13,023,132•2 73 正好是 +13,023,132•2 73,正好是 123,000,004,117,574,256,822,262,431,744,精确到无限。格式表示数字的精度仅影响它可以表示哪些数字,而不影响它表示它所表示的数字的精确程度。float
评论
binary32
123,000,004,117,574,256,822,262,431,744 的表示形式为 1.552478313446045×2⁹⁶,这与现实相对应 godbolt.org/z/39q8Eo1MM 您声称相同的数字表示为 +13,023,132×2⁷³ 是没有现实依据的。这些标准只是不禁止其他格式。binary32
ieee754.h
最后,我发现了 Java float -> decimal -> 字符串转换和 c++ float -> 字符串(十进制)转换之间的区别。我没有找到原始源代码,但我在 Java 中复制了相同的代码以使其清晰。我认为代码解释了一切:
// the context size might be calculated properly by getting maximum
// float number (including exponent value) - its 40 + scale, 17 for me
MathContext context = new MathContext(57, RoundingMode.HALF_UP);
BigDecimal divisor = BigDecimal.valueOf(2);
int tmp = Float.floatToRawIntBits(1.23e30f)
boolean sign = tmp < 0;
tmp <<= 1;
// there might be NaN value, this code does not support it
int exponent = (tmp >>> 24) - 127;
tmp <<= 8;
int mask = 1 << 23;
int fraction = mask | (tmp >>> 9);
// at this line we have all parts of the float: sign, exponent and fractions. Let's build mantissa
BigDecimal mantissa = BigDecimal.ZERO;
for (int i = 0; i < 24; i ++) {
if ((fraction & mask) == mask) {
// i'm not sure about speed, maybe division at each iteration might be faster than pow
mantissa = mantissa.add(divisor.pow(-i, context));
}
mask >>>= 1;
}
// it was the core line where I was losing accuracy, because of context
BigDecimal decimal = mantissa.multiply(divisor.pow(exponent, context), context);
String str = decimal.setScale(17, RoundingMode.HALF_UP).toPlainString();
// add minus manually, because java lost it if after the scale value become 0, C++ version of code doesn't do it
if (sign) {
str = "-" + str;
}
return str;
也许话题是无用的。谁真的需要像 C++ 那样拥有相同的实现?但至少与将浮点数转换为十进制字符串的最流行方式相比,此代码保留了浮点数的所有精度:
return BigDecimal.valueOf(1.23e30f).setScale(17, RoundingMode.HALF_UP).toPlainString();
评论
0.123e30f
float
"%.17f"
123000004117574260000000000000.00000000000000000
123000004
float
float
float