小学生学C++编程语法知识(C++文件魔法:亲手制造一个二进制文件)
上一课同学们已经知道文件本质上是一串字节。这一课我们就让孩子真正“动手造文件”用 C 把内存里的字节一个一个写进硬盘再把硬盘里的字节一个一个读回来。《C文件魔法我亲手制造一个二进制文件》一、先回忆文件到底是什么上一节我们看到hello.txt里面写ABC用十六进制编辑器打开可以看到41 42 43所以文件 ↓ 一串字节 ↓ 41 42 43而 C 程序中的变量也存在内存里。例如int x 100;可以粗略理解成内存 ┌──────┬──────┬──────┬──────┐ │ 字节 │ 字节 │ 字节 │ 字节 │ └──────┴──────┴──────┴──────┘于是产生了一个非常重要的问题能不能把内存里的这些字节直接保存到硬盘当然可以这就是write()二、write()把内存里的字节搬到文件里先看一个最简单的例子#include fstream using namespace std; int main() { char s[] ABC; ofstream fout(data.bin, ios::binary); fout.write(s, 3); fout.close(); return 0; }这个程序干了什么可以画成内存 ┌───┬───┬───┐ │ A │ B │ C │ └───┴───┴───┘ │ │ write() ↓ 硬盘上的 data.bin ┌───┬───┬───┐ │ A │ B │ C │ └───┴───┴───┘也就是说write()的工作就是把内存中的一段字节复制到文件中。三、write()的两个参数是什么意思这一句fout.write(s, 3);一定要理解。它可以翻译成从s指向的地方开始拿 3 个字节写进文件。所以fout.write(s, 3);相当于从这里开始 ↓ s │ ▼ ┌───┬───┬───┐ │ A │ B │ C │ └───┴───┴───┘ 拿 3 个字节 ↓ 文件因此write(哪里开始, 写多少字节)可以把它记成“起点 长度”四、为什么不是write(ABC, 3)实际上这样写也可以fout.write(ABC, 3);但学习的时候更推荐char s[] ABC; fout.write(s, 3);因为这样孩子更容易理解s ↓ 内存地址 ↓ 一串字节这就和我们前面学习的指针联系起来了。五、write()到底写进去的是什么这是整节课最重要的问题。很多初学者会认为fout.write(s, 3);写进去的是ABC其实更准确地说写进去的是A、B、C对应的三个字节。ASCII 情况下A ↓ 65 ↓ 0x41 ↓ 01000001所以ABC实际上可以理解成41 42 43因此fout.write(s, 3);就是把41 42 43写进文件。六、那么ios::binary是干什么的这可能是同学们容易困惑的地方。我们经常看到ofstream fout(data.bin, ios::binary);这里ios::binary到底是什么意思一句话告诉 C我要按照“原始字节”的方式处理这个文件。可以把它理解成“不要帮我翻译原样搬运”七、为什么需要ios::binary因为在某些系统上文本文件和二进制文件的处理方式可能不同。尤其经典的例子就是\n换行。在 Windows 文本模式下程序可能对换行进行特殊处理。例如内存中的\n写到文本文件时系统可能涉及\n ↓ \r\n而二进制模式强调你给我什么字节我就按照原始字节处理。所以ios::binary可以给孩子一个非常形象的记忆文本模式C “这是文本我帮你处理一下。”二进制模式C “收到一个字节都别动原样搬运。”八、但是要特别纠正一个误区很多孩子会认为ios::binary就是“把数据变成二进制”。不是这是非常重要的。数据本来就是以二进制形式存在的。ios::binary不是把数据转换成 0 和 1而是告诉文件流 按照原始字节方式处理。所以ofstream fout(data.bin, ios::binary);并不是“我要把数据变成二进制。”而是“我要按照二进制文件方式读写原始字节。”九、真正厉害的来了直接保存int前面我们保存的是char那么int x 100;能不能直接保存当然可以。#include fstream using namespace std; int main() { int x 100; ofstream fout(data.bin, ios::binary); fout.write((char*)x, sizeof(x)); fout.close(); return 0; }这段代码非常重要。我们慢慢拆开。十、sizeof(x)是什么如果int x 100;通常sizeof(x)得到4也就是说x ↓ 占 4 个字节可以画成x ┌────┬────┬────┬────┐ │ │ │ │ │ └────┴────┴────┴────┘ 4 个字节所以fout.write((char*)x, sizeof(x));实际上就是把 x 占用的这 4 个字节原封不动写进文件。十一、为什么要(char*)x这是连接“文件”和“指针”的关键。write()需要的是一块内存的起始地址。而x表示x的地址。但是write()操作的是字节所以我们把它看成char*也就是“从这个地址开始把它当成一个一个字节来看。”于是(char*)x可以暂时理解为“从 x 的地址开始把 x 的内存当成字节来看。”这就是为什么fout.write((char*)x, sizeof(x));可以直接把int的内存内容写入文件。十二、画出真正的内存搬运过程假设int x 100;假设它占 4 个字节内存 x ↓ ┌────┬────┬────┬────┐ │ xx │ xx │ xx │ xx │ └────┴────┴────┴────┘ ↑ x执行fout.write((char*)x, sizeof(x));就是内存 硬盘 ┌────┬────┬────┬────┐ │ xx │ xx │ xx │ xx │ └────┴────┴────┴────┘ │ │ write ↓ ┌────┬────┬────┬────┐ │ xx │ xx │ xx │ xx │ └────┴────┴────┴────┘不是把100转成字符串100这一点非常重要。十三、write()和的区别这也是考试和实际编程中非常容易混淆的地方。比如int x 100;使用fout x;通常是在做格式化输出最终文件里通常是字符1 0 0也就是31 30 30而fout.write((char*)x, sizeof(x));保存的是x在内存中的原始字节所以二者完全不同。十四、用一个表格记住写法做什么fout x把 x 按“人类可读的形式”写出去fout.write(...)把内存中的原始字节写出去ios::binary按二进制文件方式处理sizeof(x)x 占多少字节例如int x 100;fout x;文件里更像100writefout.write((char*)x, sizeof(x));文件里是x 在内存中的原始字节十五、既然能写就一定能读现在我们已经可以内存 ↓ write() ↓ 文件那么反过来呢当然就是文件 ↓ read() ↓ 内存这就是read()十六、read()把文件中的字节搬回内存例如int x; ifstream fin(data.bin, ios::binary); fin.read((char*)x, sizeof(x)); fin.close();这句话fin.read((char*)x, sizeof(x));可以翻译成从文件里读取sizeof(x)个字节放到x的内存里。画出来硬盘 ┌────┬────┬────┬────┐ │ xx │ xx │ xx │ xx │ └────┴────┴────┴────┘ │ │ read() ↓ 内存 ┌────┬────┬────┬────┐ │ xx │ xx │ xx │ xx │ └────┴────┴────┴────┘ ↑ x十七、完整实验先写再读我们可以写一个完整的小程序。#include iostream #include fstream using namespace std; int main() { // 第一步制造数据 int x 12345; // 第二步写入文件 { ofstream fout(data.bin, ios::binary); fout.write((char*)x, sizeof(x)); fout.close(); } // 第三步清空 x x 0; // 第四步从文件读取 { ifstream fin(data.bin, ios::binary); fin.read((char*)x, sizeof(x)); fin.close(); } // 第五步看看读回来是什么 cout x endl; return 0; }输出12345这就完成了一次写入 内存 ───────→ 文件 ↑ │ │ │ │ ↓ └───────────── 读取十八、同学们要理解文件就是“仓库”我们可以把它比喻成仓库。write()小朋友手里有玩具 ↓ 把玩具放进仓库 ↓ 文件read()仓库里面有玩具 ↓ 把玩具拿出来 ↓ 内存所以write 写进去 read 读出来非常好记。十九、一次保存多个数据比如struct Student { int age; int score; };我们有Student s; s.age 10; s.score 95;能不能直接保存可以ofstream fout(student.bin, ios::binary); fout.write((char*)s, sizeof(s)); fout.close();以后再读取Student s; ifstream fin(student.bin, ios::binary); fin.read((char*)s, sizeof(s)); fin.close();然后cout s.age endl; cout s.score endl;输出10 95这就非常有意思了。我们实际上制造了一个自己的二进制数据文件。二十、这就是“二进制数据库”的雏形比如我们有struct Student { int id; int age; int score; };保存 100 个学生Student a[100];可以fout.write((char*)a, sizeof(a));于是硬盘里就有学生1的数据 学生2的数据 学生3的数据 ... 学生100的数据这其实已经非常接近一些简单的数据文件了。孩子以后学习文件随机访问数据库存储序列化二进制协议网络通信都会再次遇到这种思想。二十一、但是这里有一个非常重要的“坑”如果我们写struct Student { int age; string name; };然后fout.write((char*)s, sizeof(s));不能简单认为这样就能正确保存name。为什么因为string里面本身还有指针、长度、容量等管理信息。你保存的可能只是string对象内部的管理结构而不是字符串真正的文字内容。这是“直接保存对象内存”最大的陷阱之一。所以int double char这种简单类型比较适合直接研究这种二进制读写。而string vector 指针就需要专门设计保存格式。这个坑以后讲“序列化”时再详细展开。二十二、还有一个更大的坑不同电脑可能不一样例如int x 100;我们直接保存它的内存fout.write((char*)x, sizeof(x));这种方式有一个问题不同机器的整数表示、字节序等可能存在差异。比如常见的小端序 Little Endian 大端序 Big Endian这也是为什么真正的网络协议文件格式数据库跨平台数据交换通常不能简单粗暴地write((char*)object, sizeof(object));就完事。不过对于我们现在学的小学生课程先把“内存中的字节 ↔ 文件中的字节”这个核心思想理解清楚比马上研究字节序更重要。二十三、把read()和write()彻底记住记住下面这张图内存 │ │ ┌────┴────┐ │ │ write() read() │ │ ↓ ↑ 文件 ←─────更准确一点write() 内存 ─────────────────→ 文件 read() 内存 ←───────────────── 文件所以write内存 → 文件read文件 → 内存二十四、再把、、write()、read()放在一起这几个符号特别容易混。可以这样记文本方式fout x;内存 ↓ 格式化 ↓ 文本文件读取fin x;文本文件 ↓ 解析 ↓ 内存二进制方式fout.write((char*)x, sizeof(x));内存 ↓ 原始字节 ↓ 文件读取fin.read((char*)x, sizeof(x));文件 ↓ 原始字节 ↓ 内存二十五、给孩子一个超级好记的口诀翻译成人话再写。把人话读进来再翻译。write()字节原样搬进去。read()字节原样搬出来。ios::binary告诉系统按原始字节处理。二十六、最后做一个“小小文件魔法师”实验同学们亲手完成。实验 1制造一个二进制文件int x 2026; ofstream fout(test.bin, ios::binary); fout.write((char*)x, sizeof(x)); fout.close();然后用 HxD 打开test.bin。观察文件里面到底是什么实验 2再把它读回来int x 0; ifstream fin(test.bin, ios::binary); fin.read((char*)x, sizeof(x)); fin.close(); cout x endl;看看2026实验 3挑战题把int x 2026;改成int a 10; int b 20; int c 30;然后fout.write((char*)a, sizeof(a)); fout.write((char*)b, sizeof(b)); fout.write((char*)c, sizeof(c));再读取fin.read((char*)a, sizeof(a)); fin.read((char*)b, sizeof(b)); fin.read((char*)c, sizeof(c));最后cout a b c endl;输出10 20 30然后再用HxD打开文件。这时候孩子会看到“原来我写的 C 变量真的变成了硬盘里的一个个字节”这就是这一课最有价值的“顿悟”。二十七、把整个知识体系串起来到这里可以把孩子已经学过的知识串成一条线C变量 │ ↓ 内存 │ │ 一个个字节 ↓ (char*)x │ ↓ write() │ ↓ 二进制文件 │ ↓ HxD │ ↓ 看到十六进制 │ ↓ 还原成二进制反过来二进制文件 │ ↓ read() │ ↓ (char*)x │ ↓ 内存 │ ↓ C变量如果孩子真正理解了这条线那么他就已经开始理解一个非常重要的计算机思想计算机世界里很多东西最终都可以落到“内存中的一串字节”。C的厉害之处就是我们可以非常接近这些字节直接控制它们。