小学生学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

其实更准确地说:

写进去的是 ABC 对应的三个字节。

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;

文件里更像:

100

write

fout.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++的厉害之处,就是我们可以非常接近这些字节,直接控制它们。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

汉克老师

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值