二进制输入输出 (binary I/O)#

这是一种序列化方式. 所谓序列化就是将对象转换为一种特定的格式 (序列化), 从而可以传输或存储, 而又能从这种特定格式转换为程序中的对象 (反序列化).

一般只是简单考一下, 记住怎么做即可.

二进制输出#

ofile.write(char const* string, streamsize count)

[string, string + count) 字符数组输出到 ofile 中, 所以我们要将对象强行转换为字符数组.

1#include <fstream>
2
3int value = 0;
4
5// ofstream 会自动为打开方式加上 std::ios_base::out, 因此直接写 std::ios_base::binary 就行了
6std::ofstream ofile("输出文件路径", std::ios_base::binary);
7ofile.write(reinterpret_cast<char const*>(&value), sizeof(value));

二进制输入#

ifile.read(char* string, streamsize count)

ifile 读入字符到 [string, string + count) 字符数组中, 所以我们要将对象强行转换为字符数组.

1#include <fstream>
2
3int value = 0;
4
5// ifstream 会自动为打开方式加上 std::ios_base::in, 因此直接写 std::ios_base::binary 就行了
6std::ifstream ifile("输入文件路径", std::ios_base::binary);
7ifile.read(reinterpret_cast<char*>(&value), sizeof(value));

std::string 二进制输出到文件后不能正常读取?#

现在, 假设我们要编写 Person 类, 它会有一个名字数据成员和一个年龄数据成员. 名字的长度很难确定, 所以我们用 std::string 作为名字的类型:

1struct Person {
2 public:
3  std::string name;
4  int age;
5};

而题目要求我们以二进制形式将 Person 输出到文件, 再从文件中读取它:

1void serialize(std::ostream& ostream, Person const& person) {
2  ostream.write(reinterpret_cast<char const*>(&person), sizeof(person));
3}
4
5void deserialize(std::istream& istream, Person& person) {
6  istream.read(reinterpret_cast<char*>(&person), sizeof(person));
7}

然而, 当我们使用这个序列化函数将 Person 输出到文件, 关闭程序, 再用反序列化函数从文件中读取 Person 时, 发现读取的内容并不正常, 甚至在之后使用时可能直接报错了!

这是因为与 int 等类型不同, std::string 除了栈上的指针还有堆上的字符数组, 而这样的 ostream.write 仅仅输出了 std::string 成员的指针部分.

栈? 堆? 如果还不了解它们也没关系, 让我们把这个问题转换为更熟悉的情况:

 1struct Widget {
 2 public:
 3  int* pointer;
 4};
 5
 6int main() {
 7  int value = 0;
 8
 9  Widget widget;
10  widget.pointer = &value;  // pointer 指向 value
11
12  std::ofstream ofile("输出文件路径", std::ios_base::binary);
13  ofile.write(reinterpret_cast<char const*>(&widget), sizeof(widget));
14};

widget 进行二进制输出, 会连同 value 一起输出出去吗? 不是的, Widget 类只有 pointer 是数据成员; value 虽然被 pointer 指向, 但不是 Widget 类的数据成员.

这样有远程 (remote) 部分 2这个说法出自《Elements of Programming》 的类都不能直接进行二进制输入输出. 如果实在需要二进制输入输出它们, 你需要根据其数据的逻辑关系 (类的不变式) 为它定义如何二进制输入输出.

提示

如果只是为了做作业, 请使用 std::array<char, 100>char array[100] 这种没有远程部分的数组解决.

这种写法只有考试喜欢. 项目如果需要序列化, 会选择用 序列化库 将数据序列化为更高效的结构, 而不是折腾这样直接的二进制输入输出.

例如, 对于 std::string, 你可以输出其长度和实际内容:

 1void serialize(std::ostream& ostream, std::string const& string) {
 2  // 长度
 3  int size = string.size();
 4  ostream.write(reinterpret_cast<char const*>(&size), sizeof(size));
 5
 6  // 实际内容                     ↓ 单个元素的大小 * 元素数量
 7  ostream.write(string.c_str(), sizeof(char) * size);
 8}
 9
10void deserialize(std::istream& istream, std::string& string) {
11  // 长度
12  int size = 0;
13  istream.read(reinterpret_cast<char*>(&size), sizeof(size));
14
15  // 根据读取字符串的长度给当前的 string 设置好长度
16  string.resize(size);
17
18  // 实际内容
19  istream.read(string.data(), sizeof(char) * size);
20}
21
22void serialize(std::ostream& ostream, Person const& person) {
23  serialize(ostream, person.name);
24  ostream.write(reinterpret_cast<char const*>(&person.age), sizeof(person.age));
25}
26
27void deserialize(std::istream& istream, Person& person) {
28  deserialize(istream, person.name);
29  istream.read(reinterpret_cast<char*>(&person.age), sizeof(person.age));
30}