将完整精度的双精度数输出到文本文件的最佳方法

22

我需要使用现有的文本文件来存储一些非常精确的值。当读取回来时,这些数字基本上需要与最初写入的数字完全相同。现在,普通人会使用二进制文件……出于许多原因,在这种情况下不可能。

那么...你们中有没有一种好的方法将双精度浮点数编码为字符串(除了增加精度)?我的第一个想法是将双精度浮点数转换为char[]并写出字符。我认为这行不通,因为其中一些字符是不可见的、能够发出声音甚至终止字符串('\0'...我指的是你!)

您有什么想法吗?

【编辑】-一旦我弄清楚了哪种提出的解决方案最适合我,我将标记一个作为“该”解决方案。


1
如果您想要可移植性,您可以假设浮点数的表示方式(标准中没有定义)。因此,唯一的可移植性方法是尽可能多地打印数字。现在,如果您想放弃可移植性,那么您可以使用二进制格式(如果需要,可以使用Base64编码)。但是,当转换为特定于平台的浮点格式时,您将失去精度(除非它与源系统完全相同)。但是,这样做与以完整精度打印相比并没有任何优势。 - Martin York
2
需要考虑的一件事是,编译器可能会将双精度浮点数分配给具有超过64位精度的CPU寄存器。当这些值被写入内存以准备写入磁盘时,它们将被截断为64位。因此,即使您以二进制形式保存双精度浮点数并读取它们,读取的值也不能保证等于原始值。 - user168715
1
无论您使用什么编码格式,当加载到可用精度较低的系统中时,都会失去精度。您是否关心它是否易于人类阅读?您是否关心它是否快速保存/加载?您是否关心存储所需的字节数?请注意,在许多高精度系统上,您可以使用“long double”来获得比“double”更高的精度。 - fuzzyTew
3
目前很少有专业格式采用二进制格式,这并非巧合。在空间不足时我们曾尝试过这样做,但从长远来看这样的选择并不划算。与二进制格式相比,人类可读的格式更易于使用和维护,并且你不会因使用人类可读格式而失去精度。在当今世界,仅基于压缩原因选择格式是一个非常糟糕的选择(除非存在某些特定原因需要这样做)。 - Martin York
3
我不会使用long double。在新的处理器上,整个x87指令集被视为过时的。例如,64bWin7似乎不允许在内核中使用x87,而Intel、AMD和Microsoft都强烈反对使用它。他们都建议改用SSE2数学运算。因此,10字节的double似乎已经过时了。 - KitsuneYMG
显示剩余2条评论
9个回答

20

如果您想保持格式的可读性,您可以这样写出双倍:

#include <iomanip>
#include <sstream>

std::string doubleToText(const double & d)
{
    std::stringstream ss;
    //ss << std::setprecision( std::numeric_limits<double>::digits10+2);
    ss << std::setprecision( std::numeric_limits<int>::max() );
    ss << d;
    return ss.str();
}

std::numeric_limits<int>::max() 将输出最大可能的十进制精度。这将在不同的浮点实现之间最精确地保留值。将该行更换为使用 std::numeric_limits<double>::digits10+2 的已注释行,将提供足够的精度,使得代码编译平台上的双精度值可以精确恢复。这将生成更短的输出并尽可能地保留双精度可以唯一表示的信息。

C++ 流操作符在读取字符串时不会保留非规格化数、无穷大和非数字。然而,POSIX 的 strtod 函数确实保留了这些信息,并且已被标准定义。因此,使用标准库调用的最精确的读取十进制数的方式是使用该函数:

#include <stdlib.h>

double textToDouble(const std::string & str)
{
    return strtod( str.c_str(), NULL );
}

1
strtod函数似乎是最简单、最完整的解决方案。 - eonil
5
std::numeric_limits<double>::max_digits10std::numeric_limits<double>::digits10+2更加规范。 - ead

10

假设使用IEEE 754双精度浮点数,printf("%.17g\n", x)将会给你足够的数字以重现原始值。


什么是解析它的最佳方式?保留无穷大和 NaN 的选项有哪些? - fuzzyTew
你可以检测所有特殊情况,比如在http://www.cplusplus.com/forum/beginner/30400/中,但通常你为什么需要那个呢?此外,十进制表示法确实很好看和易读,但你必须记住IEEE双精度浮点数中的尾数和指数都是二进制的,所以保留所有位可能会很困难...也许你需要自己的bin2dec、dec2bin函数。 - Shelwien
那个讨论串里面有很多错误的信息。除以零是在运行时完成并由FPU分配结果。 - fuzzyTew
这个想法是你可以像这样获得+ INF和- INF的编码。否则,您将只能直接访问double的位字段,请参阅http://en.wikipedia.org/wiki/Double_precision_floating-point_format。 - Shelwien
不起作用,具有大指数的非常小的数字不起作用。 - jjxtra

4
一个两步过程:首先使用二进制浮点数/双精度数序列化,然后应用base 64编码。结果不可读,但不会失去精度。

编辑:(感谢fuzzyTew和dan04)

无损十进制和人类可读的表示可能是可能的,但需要更多的空间。


2
可以创建一个可读性强的表示方式,能够精确地表示二进制浮点数。 - fuzzyTew
1
正确:2是10的因数,因此所有终止的二进制小数也在十进制中终止。尽管可能需要很多位数字,例如0.1000000000000000055511151231257827021181583404541015625。 - dan04
1
但是无法将十进制浮点数表示为二进制浮点数。我说得对吗? - Juraj Blaho
2
一般来说,不会。二进制中的1/5是0.0011 0011 0011 0011...,因此任何分母中有5因子的分数在二进制下都不会终止。 - dan04

2
您可以使用 Base 64。这将允许您在文本文件中存储精确的字节值。
我没有使用过它,但我找到了这个 C++ 的 Base 64 编码/解码

3
除了与浮点数无关之外,它与之无关。仅仅因为人们使用它来编码二进制数据,并不意味着你可以编码浮点数并期望它们能正确地输出!!! - Martin York
2
由于大多数系统都遵循IEEE 754标准,您可以将浮点数编码为二进制数据。 - fuzzyTew
1
@ fuzzyTew:没错,但如果两个平台的格式相同,你打印全精度并没有任何收益(如果两端的格式相同,并且您打印出读取的确切值,您将不会获得任何收益(如果一端截断数据,则只会失去精度)。因此,您正在牺牲可移植性,并且没有获得任何东西(我想您可能会获得更好的压缩)。 - Martin York
说得好。虽然最近的ARM VFP版本符合IEEE 754标准。我想OP需要确定应用程序需要多么可移植。 - Daniel Gallagher
@flevine100:base64可能是你需要的。它被设计用于将任意二进制转换为ASCII文本。我对它最大的问题是,你需要包含一个外部库(或编写自己的编码器和解码器)。从任何重要意义上讲,它也不是人类可读的。但如果该字段只是供你的程序读写,那么它可能是可以接受的。 - Daniel Gallagher
显示剩余5条评论

2
为了在C++中打印长列表的数字而不丢失(在相同的体系结构下写入和读取),我使用以下方法(对于double):
#include<iostream>
#include<iomanip>
#include<limits>
#include<cmath>

#include<sstream>
int main(){
std::ostringstream oss;

int prec = std::numeric_limits<double>::digits10+2; // generally 17

int exponent_digits = std::log10(std::numeric_limits<double>::max_exponent10)+1; // generally 3
int exponent_sign   = 1; // 1.e-123
int exponent_symbol = 1; // 'e' 'E'
int digits_sign = 1;
int digits_dot = 1; // 1.2

int division_extra_space = 1;
int width = prec + exponent_digits + digits_sign + exponent_sign + digits_dot + exponent_symbol + division_extra_space;

double original = -0.000013213213e-100/33215.;
oss << std::setprecision(prec) << std::setw(width) << original << std::setw(width) << original << std::setw(width) << original << '\n';
oss << std::setprecision(prec) << std::setw(width) << 1. << std::setw(width) << 2. << std::setw(width) << -3. << '\n';
}

打印
 -3.9780861056751466e-110 -3.9780861056751466e-110 -3.9780861056751466e-110
                        1                        2                       -3

总之,对于我的情况,就像是设置:

oss << std::precision(17) << std::setw(25) << original << ...;

无论如何,我可以通过以下方式测试是否有效:

    std::istringstream iss(oss.str());
    double test; iss >> test;
    assert(test == original);

1

我曾经确定过在printf中有一种特殊的格式说明符(也许是%a?)可以打印浮点数的二进制表示,但我现在找不到了..
不过,你可以尝试这个:

int main(int argc, char* argv[]){
    union fi {
        unsigned int i;
        float        f;
    } num;
    num.f = 1.23f;
    printf("%X\n", num.i);
    return 0;
}

1
没有帮助。整数或浮点数的表示都不能保证,因此将其转换为整数允许您打印一个数字,但不能保证另一个系统将生成相同的浮点值。(此外,您应该添加编译时检查,以确保float/int具有相同的大小)。 - Martin York
4
你可以按照ruslik所说的方式将输出定义为IEEE 754格式。在任何不支持这种格式的平台上,你都需要进行双精度浮点数的软件转换。 - KitsuneYMG
1
当然,这段代码是用C而不是C++编写的(疑问标记),并且使用浮点数而不是双精度浮点数——但它确实解决了问题。 - fuzzyTew

0

您没有说明为什么二进制不可用。对于您的应用程序,将二进制转换为十六进制ASCII字符串是否可行?


我的唯一限制是必须输出到一个清晰的文本文件。该文件中有其他列,用户需要使用Excel、Matlab等工具进行访问。我希望将这些数据放在同一个文件中,并编写其他工具来恢复二进制等效值。 - fbl

0

试试这个:

double d = 0.2512958125912;
std::ostringstream s;
s << d;

然后将 s 写入文件。


0

除了存储表示之外,还有类似这样的东西。 特殊值如-0、无穷大、NaN等需要特殊处理。此外,我“忘记”实现负指数。

#include <stdio.h>
#include <math.h>

const int SCALE = 1<<(52/2);

void put( double a ) {
  FILE* f = fopen( "dump.txt", "wb" );
  int sign = (a<0); if( sign ) a=-a;
  int exp2 = 0; while( a>1 ) a/=2, exp2++;
  a*=SCALE;
  int m1 = floor(a);
  a = (a-m1)*SCALE;
  int m2 = floor(a);
  fprintf(f, "%i %i %i %i\n", sign, exp2, m1, m2 );
  fclose(f);
}

double get( void ) {
  FILE* f = fopen( "dump.txt", "rb" );
  double a;
  int sign, exp2, m1, m2;
  fscanf( f, "%i %i %i %i\n", &sign, &exp2, &m1, &m2 );
  fclose(f);
  printf( "%i %i %i %i\n", sign, exp2, m1, m2 );
  a = m2; a /= SCALE;
  a+= m1; a /= SCALE;
  while( exp2>0 ) a*=2, exp2--;
  if( a<0 ) a=-a;
  return a;
}

int main( void ) {
  union {
    double a;
    unsigned b[2];
  };
  a = 3.1415926;
  printf( "%.20lf %08X %08X\n", a, b[0], b[1] );
  put( a );
  a = get();
  printf( "%.20lf %08X %08X\n", a, b[0], b[1] );
}

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