【问题标题】:Elegant binary i/o in C?C中优雅的二进制I/O?
【发布时间】:2011-05-09 17:05:53
【问题描述】:

我最近一直在使用 C/C++ 加载很多二进制文件,但我对它的不优雅感到困扰。要么我得到很多看起来像这样的代码(我已经继续前进了):

uint32_t type, k;
uint32_t *variable;
FILE *f;

if (!fread(&type, 4, 1, f))
    goto boundsError;

if (!fread(&k, 4, 1, f))
    goto boundsError;

variable = malloc(4 * k);
if (!fread(variable, 4 * k, 1, f))
    goto boundsError;

或者,我定义了一个本地的、打包的结构,以便我可以更轻松地读取固定大小的块。然而,在我看来,对于这样一个简单的问题——即将指定的文件读入内存——可以更有效地以更易读的方式完成。有没有人有任何提示/技巧等?我想澄清一下,我不是在寻找图书馆或其他东西来处理这个问题;如果我正在设计自己的文件并且不得不大量更改文件规范,我可能会受到诱惑,但现在我只是在寻找风格上的答案。

另外,你们中的一些人可能会建议mmap——我喜欢 mmap!我经常使用它,但它的问题是它会导致用于处理未对齐数据类型的讨厌的代码,而在使用 stdio 时这并不真正存在。最后,我将编写类似 stdio 的包装函数来从内存中读取。

谢谢!

编辑:我还应该澄清我不能更改文件格式——我必须阅读一个二进制文件;我无法请求其他格式的数据。

【问题讨论】:

  • mmap()!与union 一起处理未对齐的数据访问。
  • 我希望我能做到,但性能损失值得吗?这就是避免它的初衷。我的意思是,说实话,我可能会在不知不觉中在我的FILE 的缓冲区中进行未对齐的内存访问。
  • 您是否实际测量过性能影响?非对齐访问在 x86 上相对便宜。
  • 未对齐读取的问题与其说是对性能的影响(如果有的话)(几个周期的惩罚通常在这里并不重要,因为 IO 比计算贵得多),而是它将导致程序在一些根本不支持未对齐读取的 RISC 平台上崩溃。但是话又说回来,由于字节序和struct 包装差异,通过fread 阅读任何内容都会导致可移植性问题。
  • Elegant、I/O 和 C++:任选其一!

标签: c++ c binary io stdio


【解决方案1】:

您可能对protocol buffers 和其他 IDL 方案感兴趣。

【讨论】:

  • 我也建议这样做,但 OP 似乎不想要第三方库。它确实具有明显的优势,可以在不同编译器、不同平台以及同一平台上同一编译器的不同版本之间非常明确地移植。
  • 我会喜欢的,但不幸的是我没有选择输入文件格式。
【解决方案2】:

我见过的解决这个问题的最优雅的解决方案是 Sean Barrett 的 writefv,在他的小型图像编写库 stb_image_write 中使用,here 可用。他只实现了一些原语(并且没有错误处理),但是同样的方法可以扩展到基本上是二进制的printf(对于阅读,你可以做同样的事情来获得二进制scanf)。非常优雅整洁!其实整个事情就是这么简单,我不妨把它包括在这里:

static void writefv(FILE *f, const char *fmt, va_list v)
{
   while (*fmt) {
      switch (*fmt++) {
         case ' ': break;
         case '1': { unsigned char x = (unsigned char) va_arg(v, int); fputc(x,f); break; }
         case '2': { int x = va_arg(v,int); unsigned char b[2];
                     b[0] = (unsigned char) x; b[1] = (unsigned char) (x>>8);
                     fwrite(b,2,1,f); break; }
         case '4': { stbiw_uint32 x = va_arg(v,int); unsigned char b[4];
                     b[0]=(unsigned char)x; b[1]=(unsigned char)(x>>8);
                     b[2]=(unsigned char)(x>>16); b[3]=(unsigned char)(x>>24);
                     fwrite(b,4,1,f); break; }
         default:
            assert(0);
            return;
      }
   }
}

这是他使用它编写真彩色 .BMP 文件的方式:

static int outfile(char const *filename, int rgb_dir, int vdir, int x, int y, int comp, void *data, int alpha, int pad, const char *fmt, ...)
{
   FILE *f;
   if (y < 0 || x < 0) return 0;
   f = fopen(filename, "wb");
   if (f) {
      va_list v;
      va_start(v, fmt);
      writefv(f, fmt, v);
      va_end(v);
      write_pixels(f,rgb_dir,vdir,x,y,comp,data,alpha,pad);
      fclose(f);
   }
   return f != NULL;
}

int stbi_write_bmp(char const *filename, int x, int y, int comp, const void *data)
{
   int pad = (-x*3) & 3;
   return outfile(filename,-1,-1,x,y,comp,(void *) data,0,pad,
           "11 4 22 4" "4 44 22 444444",
           'B', 'M', 14+40+(x*3+pad)*y, 0,0, 14+40,  // file header
            40, x,y, 1,24, 0,0,0,0,0,0);             // bitmap header
}

write_pixels 的定义被省略,因为它在这里很切线)

【讨论】:

    【解决方案3】:

    我会稍微重构一下代码,让你的代码看起来不那么优雅,这样你的复杂数据结构就会通过一系列底层类型的调用来读取。

    我假设您的代码是纯 C 而不是 C++,因为在后者中您可能会抛出异常而不是使用 goto 语句。

    【讨论】:

    • 我看不出重构如何使它更优雅——事实上,它会使代码更混乱。除非我“解析”子结构(确实发生)或跨段,否则我宁愿不拆分我的代码。
    【解决方案4】:

    如果要反序列化二进制数据,一种选择是为要使用的结构定义序列化宏。这在 C++ 中使用模板函数和流更容易很多。 (boost::serialization 是一个非侵入式的序列化库,但是如果你想去侵入式,你可以让它更优雅)

    简单的 C 宏:

    #define INT(f,v) \
      { int _t; fread(&_t, sizeof(int), 1, f); v = ntohl(_t); }
    #define FLOAT(f,v) \
      { int _t; fread(&_t, sizeof(int), 1, f); v = ntohl(_t); /* type punning */ memcpy(&v, &_t, sizeof(float)); }
    ...
    

    用法:

      int a;
      float b;
      FILE *f = fopen("file", "rb");
    
      INT(f, a);
      FLOAT(f, b);
    

    而且,是的,序列化代码是最无聊和最脑残的代码之一。如果可以,请使用元数据描述您的数据结构,并改为机械地生成代码。有一些工具和库可以帮助您解决这个问题,或者您可以使用 Perl、Python 或 PowerShell 或其他任何工具自行开发。

    【讨论】:

    • 这里没有什么需要宏,而是使用内联函数。
    • 另外,在浮点值的二进制表示上调用 ntohl 是错误的……IEEE 浮点表示是标准化的,不会随机器字节顺序而变化。
    • C 中的内联函数不能按类型重载,并且您没有引用语义,因此您必须添加一个“&”,从而创建字段/变量的别名.或者您必须改用返回值调用约定。那是另一种风格。
    • IEEE 浮点表示与机器上的单词表示匹配。因此,*(int *)&float 在 little-endian 和 big-endian 机器上具有相同的 value。然而,包含符号位的字节在小端机器上是从开始的第三个字节,但在大端机器上是从开始的第零字节。因此,如果您必须对 int 的值使用 ntohl(),则还必须将其用于浮点数。
    • @Ben Jon 是对的,IEEE 浮点数定义了 32 位/64 位值内的浮点数/双精度的位布局,而不是字节顺序。
    【解决方案5】:

    数组读取部分看起来值得拥有自己的可重用功能。除此之外,如果您确实有 C++ 可用(从问题中并不完全清楚),那么硬编码变量的大小是不必要的,因为可以从指针中推断出大小。

    template<typename T>
    bool read( FILE* const f, T* const p, size_t const n = 1 )
    {
         return n * sizeof(T) == fread(f, sizeof T, n, p);
    }
    
    template<typename T>
    bool read( FILE* const f, T& result )
    {
         return read(f, &result);
    }
    
    template<typename Tcount, typename Telement>
    bool read_counted_array( FILE* const f, Tcount& n, Telement*& p )
    {
         if (!read(f, n) || !(p = new Telement[n]))
             return false;
         if (read(f, p, n))
             return true;
         delete[] p;
         p = 0;
         return false;
    }
    

    然后

    uint32_t type, k;
    uint32_t *variable;
    FILE *f;
    
    if (read(f, type) &&
        read_counted_array(f, k, variable) && ...
       ) {
       //...
    }
    else
        goto boundsError;
    

    当然,如果将数据传递给假定使用了malloc 的代码,请随意继续使用mallocfree 而不是new[]delete[]

    【讨论】:

      【解决方案6】:

      这是我想出的一些 C99 代码:

      你的例子应该是:

      #include "read_values.h"
      #include "read_array.h"
      
      assert(sizeof (uint32_t) == 4);
      
      uint32_t type, k;
      uint32_t *variable;
      FILE *f;
      
      _Bool success =
          read_values(f, "c4c4", &type, &k) &&
          read_array(f, variable, k);
      
      if(!success)
      {
          /* ... */
      }
      

      【讨论】:

        猜你喜欢
        • 2015-01-29
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2014-03-12
        • 2015-05-24
        相关资源
        最近更新 更多