任何双精度浮点数所需的最大字符长度是多少?

71
当我将无符号8位整数转换为字符串时,我知道结果最多只有3个字符(对于255),而对于有符号的8位整数,例如“-128”,我们需要4个字符。
现在我真正想知道的是浮点数值的相同情况。要将任何“double”或“float”值表示为字符串所需的最大字符数是多少?
假设使用常规C/C++ double(IEEE 754)和常规十进制展开(即没有%e printf-formatting)。
我甚至不确定非常小的数字(即0.234234)是否会比表示整数的双倍更长?

7
Jalf,为什么有人会提到那个?谁说他在问固定大小的缓冲区需要多大?也许他想知道在基于文本的表格中他需要在控制台上预留多少字符列。 - Rob Kennedy
没有科学计数法,对于数量级范围极端的值来说,它将变得很长,但这有什么意义呢?谁会读这样一个数字——一个双精度浮点数(通常)具有大约15个有效十进制数字——其余的将是大量的前导零或尾随零。 - Clifford
1
不,你可以有比15个有效数字更多的小数位,但整数只能有15个有效数字。这是因为虽然你可以表示所有整数,但不能表示所有十进制扩展,所以可以使用较少的位来覆盖更大的范围。 - martin
我不是为了让人们阅读而打印数字,我正在尝试找到所需的字符缓冲区大小,以确保 strtod 的反转(即 "dtoa(double d, char* output)") 可以安全地完成,没有缓冲区溢出的风险。 - martin
@matrin,我尝试使用for循环并将数字乘以它直到得到1.#INF00,最大的数字长度为286字节。所以我猜你用512字节应该没问题吧?(使用printf)。 - Rookie
@jalf 问题在于我不知道如何使用标准的 printf 格式来格式化 std::string。所以如果我得到一个双精度浮点数,我需要创建一个 char[X] 缓冲区。 - Tomáš Zato
12个回答

42

C语言中的标准头文件<float.h>或C++中的<cfloat>包含了与浮点类型的范围和其他度量有关的常量,其中之一是DBL_MAX_10_EXP,表示表示所有double值所需的最大10的幂指数。由于表示1eN需要N+1个数字,并且可能还有一个负号,所以答案是

int max_digits = DBL_MAX_10_EXP + 2;

假设指数大于表示最大有效数字所需的位数,否则会有小数点和更多数字。

更正

实际上,最长的数字是最小可表示的负数:它需要足够的数字来覆盖指数和有效数字。这个值为-pow(2, DBL_MIN_EXP - DBL_MANT_DIG),其中DBL_MIN_EXP为负数。很容易看出(并归纳证明),非科学计数法的十进制表示形式-pow(2,-N)需要3+N个字符("-0.",后跟N个数字)。因此答案是

int max_digits = 3 + DBL_MANT_DIG - DBL_MIN_EXP

对于一个64位的IEEE双精度浮点数,我们有

DBL_MANT_DIG = 53
DBL_MIN_EXP = -1023
max_digits = 3 + 53 - (-1023) = 1079

2
抱歉,这是错误的 - 最长的数字将是非常小的数字,而不是非常大的数字,并且计算它们的长度更加复杂。应该可以从DBL_MIN_EXP和DBL_MANT_DIG中计算出来;如果我能算出来,我会更新答案。 - Mike Seymour
5
由于DBL_MIN_EXP是以位为单位而不是数字为单位,因此这里的max_digits计算似乎不正确。 - Soren
8
开玩笑吧...1079位数字?你能否编写一个示例C程序,在标准输出中生成该数字? - Tomáš Zato
3
这个回答仍然是错误的。打印任何十进制double值(即以"%f"格式)所需的最大字符数将为-DBL_MIN的值(即-0x1p-1022,假设您的double是二进制64 IEEE 754)。为此,您需要确切地325个字符。也就是说:DBL_DIG + abs(DBL_MIN_10_EXP)+ strlen("-0.")。当然,这是因为log10(fabs(DBL_MIN))为308,它也是abs(DBL_MIN_10_EXP)+1(+1是因为小数点左边的前导数字),这是前导零位于显着数字左侧的数量。 - Greg A. Woods
2
printf("%0.320lf\n", -DBL_MIN); - David Schwartz
显示剩余8条评论

26
根据IEEE 754-1985,双精度浮点类型表示的最大值(即:

 

-2.2250738585072020E-308

需要24个字符来表示。


4
酷!你能解释一下为什么这是最长的吗?例如,一个非常小的0.033211233457645...234234为什么不能变得更长? - martin
2
如果你不想使用科学计数法,这会给你多308个字符... - Vilx-
2
由于双精度浮点数的尾数,遵循IEEE 754-1985标准,可以表示小数点后最大17位数字的精度。在此基础上加上尾数、负号、点、e字符和3位周期(8位),你将得到准确的24个字符。 - Vitaliy Ulantikov
3
0.00000000000000000… 约为 308… 00002225073858507202。 - pmg
@VitaliyUlantikov 你好,能否告诉我为什么IEEE 754-1985可以在小数点后表示最高精度达到17位数字。为什么是17位数字呢?谢谢。 - user1024
显示剩余6条评论

13
一个比IEEE-754规范更详细的正确信息来源是UC伯克利分校的讲义第4页,再加上一些DIY计算。这些讲义幻灯片也适合工程学生。

推荐缓冲区大小

| Single| Double | Extended | Quad  |
|:-----:|:------:|:--------:|:-----:|
|   16  |  24    |    30    |  45   |

这些数字基于以下计算:

整数部分的最大十进制位数

| Single| Double | Extended | Quad  |
|:-----:|:------:|:--------:|:-----:|
|   9   |   17   |    21    |  36   |

* Quantities listed in decimals.

十进制计数基于以下公式:最多为 Ceiling(1 + NLog_10(2)) 位小数,其中 N 是整数部分的位数*。

最大指数长度

| Single| Double | Extended | Quad  |
|:-----:|:------:|:--------:|:-----:|
|   5   |   5    |     7    |   7   |
* Standard format is `e-123`.

最快的算法

打印浮点数的最快算法是Grisu2算法,该算法在研究论文《快速准确地打印浮点数》中有详细介绍。我能找到的最佳基准测试可以在这里找到。


5
你可以使用 snprintf() 来检查所需的字符数。 snprintf() 返回打印传递给它的任何内容所需的字符数。
/* NOT TESTED */
#include <stdio.h>
#include <stdlib.h>
int main(void) {
    char dummy[1];
    double value = 42.000042; /* or anything else */
    int siz;
    char *representation;
    siz = snprintf(dummy, sizeof dummy, "%f", value);
    printf("exact length needed to represent 'value' "
           "(without the '\\0' terminator) is %d.\n", siz);
    representation = malloc(siz + 1);
    if (representation) {
        sprintf(representation, "%f", value);
        /* use `representation` */
        free(representation);
    } else {
        /* no memory */
    }
    return 0;
}

注意:`snprintf()`是C99函数。如果C89编译器将其作为扩展提供,则可能无法执行上面程序的预期操作。
编辑:将链接更改为实际描述C99标准强制执行的功能的链接;原始链接中的描述是错误的。
2013年:将链接返回到我更喜欢的POSIX网站,而不是第一个编辑的网站

1
现在要回答这个问题,只需要弄清楚要分配给 double value = ??; 的数字是什么。 - DrBeco

2

1024 远远不够,最小的负 double 值有 1077 个十进制位数。以下是一些 Java 代码。

double x = Double.longBitsToDouble(0x8000000000000001L);
BigDecimal bd = new BigDecimal(x);
String s = bd.toPlainString();
System.out.println(s.length());
System.out.println(s);

这是程序的输出。
1077
-0.000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004940656458412465441765687928682213723650598026143247644255856825006755072702087518652998363616359923797965646954457177309266567103559397963987747960107818781263007131903114045278458171678489821036887186360569987307230500063874091535649843873124733972731696151400317153853980741262385655911710266585566867681870395603106249319452715914924553293054565444011274801297099995419319894090804165633245247571478690147267801593552386115501348035264934720193790268107107491703332226844753335720832431936092382893458368060106011506169809753078342277318329247904982524730776375927247874656084778203734469699533647017972677717585125660551199131504891101451037862738167250955837389733598993664809941164205702637090279242767544565229087538682506419718265533447265625

1
// 打印大于1077的一个数:import java.math.BigDecimal; public class Program { public static void print_bigdecimal(String name, BigDecimal bd) { String s = bd.toPlainString(); System.out.println ("NUM " + name + ": " + s + " (" + s.length() + " 个字符)"); } public static void main(String[] args) { print_bigdecimal("-2^-1075==", new BigDecimal(-1).divide (new BigDecimal(2).pow(1075))); print_bigdecimal("0x80...01L", new BigDecimal(Double.longBitsToDouble(0x8000000000000001L))); } } - martin
2^-1075是有效的,因为它等于2^-52 + 2^-1023。 - martin
你所看到的是一个糟糕的打印算法。在这种边缘情况下,应该使用Grisu3算法。 - user2356685
“边缘情况”?即使是非常小的值也是有效的“小值”-没有更多,也没有更少。tempreal 10字节实数类型甚至允许更小的值。gmplib库则更小。 - Olof Forshell
https://float.exposed/0x8000000000000001 - Boris Verkhovskiy
https://float.exposed/0x8000000000000001 - user3064538

1

根据Greg A. Woods准确的评论,对接受的答案进行改进,需要更为保守但仍足够的字符数为3 + DBL_DIG + -DBL_MIN_10_EXP(总共325个字符),其中3代表可能需要的前导“-0.”。如果使用C风格的字符串,则需添加一个空(null)字符('\0')以便创建充分大小(大小为326)的缓冲区:

#include <limits.h>

char buffer[4 + DBL_DIG + -DBL_MIN_10_EXP];

对于那些更喜欢 C++ 数值限制接口 的人,可以使用以下代码:

#include <limits>

char buffer[4 + std::numeric_limits<double>::digits10 + -std::numeric_limits<double>::min_exponent10];

请考虑使用DBL_DECIMAL_DIG(而非DBL_DIG)以获取一个可以转换回原始双精度数的字符串。 - MattTT

1

当您将float/double转换为字符串时,可以通过设置精度来控制字符串表示中的数字位数。此时,最大数字位数将等于以您指定的精度为标准的std::numeric_limits<double> :: max()的字符串表示。

#include <iostream>
#include <limits>
#include <sstream>
#include <iomanip>

int main()
{
 double x = std::numeric_limits<double>::max();

 std::stringstream ss;
 ss << std::setprecision(10) << std::fixed << x;

 std::string double_as_string = ss.str();
 std::cout << double_as_string.length() << std::endl;
}

所以,具有精度为10的double类型的最大位数为320位。


1

这取决于你所说的“表示”。十进制小数没有精确的浮点表示。当你将十进制小数转换为二进制小数再转换回十进制时,你没有精确的十进制表示,并且在二进制表示的末尾会有噪声位。

问题没有涉及从十进制开始,但所有源代码(和必须用户输入)都是十进制的,并涉及可能的截断问题。在这种情况下,“精确”是什么意思?

基本上,这取决于你的浮点表示。

如果你有48位的尾数,这大约需要16个十进制数字。指数可能是剩余的14位(大约5个十进制数字)。

经验法则是,位数约为十进制数字的3倍。


5
实际上,它们不能拥有无限的数字,因为任何二进制小数都可以在十进制表示中精确地表达。 - Vilx-
我的意思是以精确的十进制展开形式表示(例如没有%e之类的东西)。我假设我们使用IEEE 754标准的C/C++双精度浮点数。 - martin
当然。我的意思是,双精度浮点数在内存中以二进制表示,例如1101010.101000110101。二进制小数点后的位数是有限的,因此可以在十进制中精确表示。1/2表示为0.5;1/4表示为0.25;1/8表示为0.125;等等。我有遗漏什么吗? - Vilx-
问题正好相反。每个十进制小数都没有一个精确的二进制表示。所以,如果你的“原始”数字是一个十进制数,那么从浮点数到十进制数的转换一开始就是错误的。如果你要与一个十进制数进行比较,那么十进制的小数位数并不重要。 - S.Lott
4
当然,十进制小数不能在二进制小数中精确表示。我从未说过相反的话。但是,二进制小数可以在十进制小数中精确表示,因此在我看来,“浮点数没有精确的十进制表示,并且可能具有无限数量的重复小数位”并不正确。 - Vilx-
@Vilx:我不同意。我会说:“当然,并非所有的十进制小数都能够在二进制小数中精确表示。” - Olof Forshell

1
“用多少个字符才能表示任何double值的最大长度是多少?”
这个问题的确切答案是:8个ASCII字符-十六进制格式,不包括“0x”前缀-100%准确 :)(但这不仅仅是一个玩笑)。
IEEE-754双精度的可用精度约为16位小数位,因此除了教育目的外,超过该精度的表示法只会浪费资源和计算能力:
用户看到屏幕上的700位数字时,并不能获取更多信息。
以“更精确”的形式存储的配置变量是无用的-每次对这样的数字进行操作都会破坏精度。(排除更改符号位)
如果有人需要更好的真实精度,那么就有约18位精度的80位长双精度或f.e. libquadmath。
祝好!

0

在打印任何十进制 double 值(即以 "%f" 格式)所需的最大字符数将是对于值 -DBL_MIN(即 -0x1p-1022,假设二进制64位 IEEE 754 是您的 double),需要确切的325个字符。这是: DBL_DIG + abs(DBL_MIN_10_EXP) + strlen("-0.")。当然,这是因为 log10(fabs(DBL_MIN)) 是308,这也是 abs(DBL_MIN_10_EXP)+1(+1 是因为小数点左侧的前导数字),这是有效数字左侧的前导零的数量。

int lz;                 /* aka abs(DBL_MIN_10_EXP)+1 */
int dplaces;
int sigdig;             /* aka DBL_DECIMAL_DIG - 1 */
double dbl = -DBL_MIN;

lz = abs((int) lrint(floor(log10(fabs(dbl)))));
sigdig = lrint(ceil(DBL_MANT_DIG * log10((double) FLT_RADIX)));
dplaces = sigdig + lz - 1;
printf("f = %.*f\n", dplaces, dbl);

我的输出是"f = -0.00000000(300个零)000000000000000",而printf("lz = %d, sigdig = %d, dplaces = %d, dbl = %g\n", lz, sigdig, dplaces, dbl);则打印出"lz = 308, sigdig = 16, dplaces = 323, dbl = -2.22507e-308"。你收到了什么输出? - chux - Reinstate Monica
lrint() 调用与 (int) 相比提供了很少的价值。移除它将增加清晰度。 - chux - Reinstate Monica
sigdig 公式在 FLT_RADIX == 2 时会偏差 1。请参考 DBL_DECIMAL_DIG 的定义,这实际上是用来确定所有 doublesignificant digits 的。建议将 sigdig = DBL_DECIMAL_DIG;(例如 17 - chux - Reinstate Monica
注意,DBL_MIN 是最小的正常值。 DBL_TRUE_MIN 是最小的非零双精度浮点数。当然,使用 DBL_MIN 将足以打印 有效数字 - 正如此答案所示。 - chux - Reinstate Monica
正如我在评论中所说的,sigdig实际上就是DBL_DECIMAL_DIG - 1 - Greg A. Woods
显示剩余4条评论

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