STD 中的浮点数到字符串转换实现

float number to string converting implementation in STD

提问人:Valera Dubrava 提问时间:7/7/2022 最后编辑:Valera Dubrava 更新时间:10/23/2022 访问量:330

问:

我遇到了一个奇怪的问题。请看这个简单的代码:

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)。它以十进制格式生成大量数字。该数字的精度比浮点数高得多。看起来这是一个问题,因为字符串表示包含浮点数不能包含的内容。

C++ printf 标准 浮点精度

评论

0赞 Richard Critten 7/7/2022
对我来说,看起来不错是一个 30 位数字,但只有大约 7 到 9 位的精度,所以其余的都是“发明”的。格式字符串指定小数点后有 17 位数字的非科学记数法。0.123e30ffloat"%.17f"
0赞 Valera Dubrava 7/7/2022
Java实现给了我这个数字。123000004117574260000000000000.00000000000000000
1赞 Richard Critten 7/7/2022
这里之后的任何内容都不存在于 .您可以从 a 中获得的只是在 7 到 9 位数字之间。读过浮点数学坏了吗?123000004floatfloat
3赞 molbdnilo 7/7/2022
最接近 0.123e30 的 IEEE 754 123000004117574256822262431744。float
1赞 molbdnilo 7/7/2022
也许值得注意的是,Java 结果也不能表示为 IEEE 754 浮点数,这在我看来是坏的。

答:

0赞 Marek R 7/7/2022 #1

请阅读文档:

printf, fprintf, sprintf, snprintf, printf_s, fprintf_s, sprintf_s, snprintf_s - cppreference.com

格式字符串由普通的多字节字符(除外)和转换规范组成,这些字符原封不动地复制到输出流中。每个转换规范都具有以下格式:%

  • 介绍性人物%
  • ...
  • (可选)后跟整数或 ,或者两者都不指定转换精度。在使用 when 的情况下,精度由 int 类型的附加参数指定,该参数出现在要转换的参数之前,但如果提供了最小字段宽度,则出现在提供最小字段宽度的参数之后。如果此参数的值为负数,则忽略该参数。如果既不使用数字,也不使用,则精度为零。有关精度的确切影响,请参见下表。.***

....

转换说明符 解释 预期参数类型
f F 浮点数转换为样式为 [-]ddd.ddd 的十进制表示法。精度指定小数点字符后显示的确切位数。默认精度为 6。在替代实现中,即使后面没有数字,也会写入小数点字符。有关无穷大和非数字转换样式,请参阅注释。

因此,对于您强制形式(无指数),并且您被迫在小数点分隔符后显示 17 位数字。如此大的价值,打印结果看起来很奇怪。fddd.ddd.17

-1赞 Eric Postpischil 7/8/2022 #2

您正在使用的 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”没有直接存储。float0.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

评论

0赞 Maxim Egorushkin 7/9/2022
binary32123,000,004,117,574,256,822,262,431,744 的表示形式为 1.552478313446045×2⁹⁶,这与现实相对应 godbolt.org/z/39q8Eo1MM 您声称相同的数字表示为 +13,023,132×2⁷³ 是没有现实依据的。这些标准只是不禁止其他格式。binary32
0赞 Maxim Egorushkin 7/9/2022
以下是 Nvidia docs.nvidia.com/cuda/floating-point/index.html#formats 的 IEEE 754 格式的最新概述
0赞 Eric Postpischil 7/9/2022
IEEE 754-2019 第 3.3 条说,我们可以将有效数视为整数:“将有效数视为整数......(-1)^s×b^q×c 形式的浮点数,其中...c 是一个数字,由数字字符串表示,格式为 d_0 d_1 d_2...d_(p-1),其中 d_i 是整数 0≤d_i<b(因此 c 是 0≤c≤b^p 的整数)。(全文和格式见原文;此处无法复制。GNU 和任何 Nvidia 文档都无法取代这一点。ieee754.h
0赞 Maxim Egorushkin 7/10/2022
您将视图二进制表示混淆了。
0赞 Eric Postpischil 7/10/2022
不,我不是。请参阅引用段落的全文。
0赞 Valera Dubrava 7/8/2022 #3

最后,我发现了 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();