大家好!
我在二进制文件方面遇到了一些问题,并创建了一个帖子,Disch 提供了很大的帮助,我觉得最好不要让那个帖子仅仅停留在那个帖子里。(帖子链接:文章底部)
这篇文章是对这篇文章的背景介绍
Disch 的良好二进制文件教程
在这篇文章中,你不会看到“如何向二进制文件写入数据”,而是会看到“为什么我们不应该向二进制文件写入大于 1 字节的变量和数据。”
开始吧
当你对内存块进行原始写入时,`write()` 会查看你给它的指针,并盲目地将 X 字节复制到文件中。对于 POD(纯旧数据)类型,这 sort of 有效……但对于复杂类型(如字符串),则完全无效。
让我们看看为什么。
****为什么你不应该读/写复杂的非 POD 结构/类****
原因 #1:复杂类型可能包含动态分配的内存或其他指针
这里有一个简化的例子
1 2 3 4 5 6 7 8 9
|
class Foo
{
private:
int* data;
public:
Foo() { data = new int[10]; }
~Foo() { delete[] data; }
};
|
在这里……我们的 `Foo` 类在概念上包含 10 个 int(约 40 字节)的信息。但如果你执行 `sizeof(Foo)`……它可能会给你一个指针的大小(约 4 字节)。
这是因为 `Foo` 类不包含它所指向的数据……它只包含一个指向它的指针。因此……对文件进行一次简单写入只会写入指针,而不是实际数据。
之后尝试读取该数据只会导致得到一个指向随机内存的指针。
这与字符串的运作方式类似。字符串数据实际上不在字符串类中……而是动态分配的。
#2:非 POD 类型可能包含 VTables 和其他你绝对不能触碰的“隐藏”数据
简化的例子
1 2 3 4 5 6
|
class Foo
{
public:
virtual ~Foo() { }
int x;
};
|
`sizeof(Foo)` 可能会比 `sizeof(int)` 大,因为 `Foo` 现在是多态的……这意味着它有一个 VTable。VTables 是黑魔法,你绝对不能随意摆弄它们,否则你就有可能毁掉你的程序。
但同样……一次简单的读/写并不会注意到这一点……只会尝试读/写整个对象……包括 vtable。结果就是严重的错误。
所以是的。简单的读/写对复杂类型无效,除非它们是 POD。
但如果你注意到,我之前说过 POD 类型只“sort of”有效。这是什么意思?
****为什么你不应该读/写 POD 结构/类****
让我们再看一个简单的例子
1 2 3 4 5 6
|
struct Foo
{
char a; // 1 byte
int b; // 4 bytes
char c; // 1 byte
};
|
这里我们有一个 POD 结构。它不会出现前面提到的任何问题。我添加了注释来说明每个单独变量可能占用的字节数(技术上这可能有所不同,但这是典型的)。
所以,如果一个结构只是所有这些变量的集合……你期望结构的大小等于它们的总和……对吗?那么 `sizeof(Foo)` 应该是 6?
嗯……在我的机器上 `sizeof(Foo)` 是 12。惊喜!
发生的情况是,编译器正在为结构添加填充,以便变量在某些内存边界上对齐。这使得访问它们更快。
所以,当你对文件进行一次简单的、原始的写入时,它也会写入填充字节。当然,当你读取它时……你会读取填充字节,并且如你所料,它会起作用。
那么为什么我说它只 sort of 有效呢?
嗯,考虑以下情况。
- 你运行你的程序并保存了一些文件。
- 你将你的程序移植到另一个平台和/或更改或更新你的编译器
- 这个新的编译器碰巧为该结构分配了不同的填充
- 你运行新编译的程序,并尝试加载你保存在旧版本程序中的文件
由于填充已更改,数据将被以不同的方式读取(读取的数据更多或更少,或者填充在不同的位置)——因此读取失败,你会得到垃圾数据。
有一些方法可以告诉编译器去掉填充。但这会引起其他我现在不想深入探讨的问题。我们只说内存对齐很重要。
所以,好的……简单来说……一次性读/写结构并不是一个好主意。所以只读/写单个变量可以……对吧?
嗯……
****为什么你不应该读/写任何大于 1 字节的变量****
有 2 件事情你需要注意。
#1:变量大小定义不明确。`
int` 可能根据你的平台/编译器是 4 字节……或者可能是 2 字节,或者可能是 8 字节。
所以读/写一个完整的`
int` 会遇到与上面“填充”场景相同的问题。如果你有一个用你的程序 X 版本保存的文件,然后在 Y 版本中重新构建,其中 int 的大小突然改变了……你的文件将不再加载。
这可以通过使用`
` 类型,如 `uint8_t`、`uint16_t` 等来解决,这些类型都保证具有特定的字节大小。
#2:字节序。内存由一系列字节组成。一个 int 在内存中如何存储,当你进行原始写入时,它在文件中就是如何存储的。但是 int 在内存中如何存储,取决于你运行的机器。
x86/x64 机器是小端序。所以如果你有一个 `int foo = 1;`,在内存中 foo 会是这样的
01 00 00 00
所以,如果你在你的 x86 机器上将 `foo` 保存到文件中……然后将该文件交给你的朋友,他运行的是大端序机器……他会以同样的方式读回它。
然而……在大端序机器上……`01 00 00 00
` 不是 1……而是 0x1000000……或者 **16777216**
所以是的……你的加载失败了,你的程序也崩溃了。
这就是为什么我坚持永远不要向二进制文件读/写任何大于单个字节的内容。这样做可以确保你的文件始终有效。
考虑到这一点……我写了一篇文章,解释了如何只通过读/写单个字节来完成所有的二进制文件 IO。这包括如何读/写字符串。
文章在这里
http://www.cplusplus.com/articles/DzywvCM9/
这是 Disch 最初的论坛帖子
http://www.cplusplus.com/forum/beginner/108114/#msg587223