• 文章
  • (Disch 著) 不要向二进制文件写入任何变量
发布
2013年11月8日 (最后更新:2013年11月8日)

(Disch 著) 不要向二进制文件写入任何大于 1 字节的变量

评分:3.5/5 (43 票)
*****
大家好!
我在二进制文件方面遇到了一些问题,并创建了一个帖子,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