• 文章
  • Disch 的优秀二进制文件教程
发布者:
2012 年 8 月 8 日 (最后更新:2012 年 8 月 8 日)

Disch 的优秀二进制文件教程

评分:4.3/5 (264 票)
*****
****=== 正确的二进制文件 I/O 方法 ===****

1) 定义你的构建块
二进制文件,在其核心,不过是一系列字节。这意味着任何大于字节(注意:几乎所有东西)的东西都需要以字节为单位来定义。对于大多数基本类型来说,这很简单。

C++ 提供了一些常用的整数类型。有 charshortintlong(以及其他)。

这些类型的问题在于它们的大小没有被很好地定义。`int` 在一台机器上可能是 8 字节,但在另一台机器上可能只有 4 字节。唯一一致的是 `char`... 它保证始终是 1 字节。

对于你的文件,你需要定义你自己的整数类型。
这里有一些基础

u8 = 无符号 8 位 (1 字节) (即:unsigned char)
u16 = 无符号 16 位 (2 字节) (即:unsigned short -- 通常)
u32 = 无符号 32 位 (4 字节) (即:unsigned int -- 通常)
s8, s16, s32 = 以上类型的有符号版本

u8 和 s8 都是 1 字节,所以它们实际上不需要被定义。它们可以“按原样”存储。但对于较大的类型,你需要选择一种字节序。

在本例中,我们选择小端序,这意味着一个 2 字节的变量 (u16) 将先存储低字节,再存储高字节。因此,当在十六进制编辑器中检查文件时,值 0x1122 将在文件中显示为 22 11

使用 iostream 安全读写 u16 的示例方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
u16 ReadU16(istream& file)
{
  u16 val;
  u8 bytes[2];

  file.read( (char*)bytes, 2 );  // read 2 bytes from the file
  val = bytes[0] | (bytes[1] << 8);  // construct the 16-bit value from those bytes

  return val;
}

void WriteU16(ostream& file, u16 val)
{
  u8 bytes[2];

  // extract the individual bytes from our value
  bytes[0] = (val) & 0xFF;  // low byte
  bytes[1] = (val >> 8) & 0xFF;  // high byte

  // write those bytes to the file
  file.write( (char*)bytes, 2 );
}


u32 的处理方式相同,但你需要将其分解并以 4 个字节而不是 2 个字节来重构它。


2) 定义你的复杂类型
这里主要是字符串,所以我将重点介绍字符串。

有几种存储字符串的方法。

1) 你可以规定它们是固定宽度。例如:你的字符串将以 128 字节的宽度存储。如果实际字符串较短,文件将被填充。如果实际字符串较长,写入文件的数据将被截断(丢失)。
- 优点:实现最简单
- 缺点:如果有大量短字符串,文件空间使用效率低下,字符串的最大长度受到限制。

2) 你可以使用 C 字符串的“空终止符”来标记字符串的结尾
- 优点:任意长度的字符串。
- 缺点:字符串中不能嵌入空字符。如果你的字符串在写入时包含空字符,它将导致文件加载不正确。可能是最难实现的

3) 你可以写入一个 u32 来指定字符串的长度,然后在其后写入字符串数据。
- 优点:任意长度的字符串,可以包含任何字符(包括空字符)。
- 缺点:每个字符串有 4 个额外的字节,使得空间效率比方法 #2 稍低(但也不是很多)。


我更倾向于选项 #3。以下是可靠地读写二进制文件字符串的示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
string ReadString(istream& file)
{
  u32 len = ReadU32(file);

  char* buffer = new char[len];
  file.read(buffer, len);

  string str( buffer, len );
  delete[] buffer;

  return str;
}

void WriteString(istream& file, string str)
{
  u32 len = str.length();

  WriteU32(file, len);
  file.write( str.c_str(), len );
}


向量/列表/等也可以用类似的方式处理。你开始写入大小作为 u32,然后读取/写入文件中相应数量的单个元素。


3) 定义你的文件格式
这是关键。既然你已经定义了术语,你就可以构建你想要的文件外观。我通常会打开一个文本编辑器,在一个看起来像这样的页面上勾勒出它

1
2
3
4
5
6
7
char[4]      header     "MyFi" - identifies this file as my kind of file
u32          version    1 for this version of the spec

u32          foo        some data
string       bar        some more data
vector<u16>  baz        some more data
...


这勾勒出了文件将如何外观/行为。举个例子,如果你在十六进制编辑器中查看此文件,你会看到这样

1
2
4D 79 46 69 01 00 00 00  06 94 00 00 03 00 00 00
4D 6F 6F 02 00 00 00 EF  BE 0D F0


由于文件格式定义得如此清晰,仅仅检查这个文件就能告诉你文件确切的包含内容。

前 4 个字节:4D 79 46 69 - 这些是字符串 "MyFi" 的 ASCII 码,它将此文件标识为我们类型的文件(而不是 wav 或 mp3 文件之类的,它们将有不同的头)。

接下来的 4 个字节:01 00 00 00 - 字面值 1,表示此文件是“版本 1”。如果你以后决定修改此文件格式,你可以使用此版本号来支持读取旧文件。

接下来的 4 个字节用于我们的“foo”数据:06 94 00 00 表示 foo == 0x9406

之后是一个字符串(“bar”)。字符串以 4 个字节开始表示长度:03 00 00 00 表示长度为 3。所以接下来的 3 个字节 4D 6F 6F 形成了字符串的 ASCII 数据(在本例中是:“Moo”)

之后是我们的向量(“baz”)。想法一样... 以 4 个字节开始表示长度:02 00 00 00,表示长度为 2
然后文件中有两个 u16。第一个是 EF BE (0xBEEF),第二个是 0D F0 (0xF00D)


你会发现所有常见的二进制文件格式,如 .zip, .rar, .mp3, .wav, .bmp 等等,都是这样定义的。它绝对不留任何偶然。




鸣谢 Disch,他写了所有这些,我只是把它复制在这里,因为
(Disch 在上面教程之后的那篇帖子中写了这些)

我真的应该把这些写成文章而不是论坛帖子。真是的。有人想帮我把这个转成文章吗?我现在太懒了。
好吧 Disch,我已经帮你把这个转成文章了!希望大家喜欢!