****=== 正确的二进制文件 I/O 方法 ===****
1) 定义你的构建块
二进制文件,在其核心,不过是一系列字节。这意味着任何大于字节(注意:几乎所有东西)的东西都需要以字节为单位来定义。对于大多数基本类型来说,这很简单。
C++ 提供了一些常用的整数类型。有
char
、
short
、
int
和
long
(以及其他)。
这些类型的问题在于它们的大小没有被很好地定义。`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,我已经帮你把这个转成文章了!希望大家喜欢!