【c语言】文件操作详解 - 从打开到关闭

文章目录
- 1. 为什么使用文件?
- 2. 什么是文件?
- 3. 如何标识文件?
- 4. 二进制文件和文本文件?
- 5. 文件的打开和关闭
- 5.1 流和标准流
- 5.1.1 流
- 5.1.2 标准流
- 5.2 文件指针
- 5.3 文件的打开和关闭
- 6. 文件的读写顺序
- 6.1 顺序读写函数
- 6.2 对比一组函数
- 7. 文件的随机读写
- 7.1 fseek
- 7.2 ftell
- 7.3 rewind
- 8. 文件读取结束的判定
- 8.1 被错误使用的`feof`
- 9. 文件缓冲区
1. 为什么使用文件?
如果没有文件,我们写的程序的数据存储在电脑的内存当中,如果程序退出,内存回收,数据就丢失了,再次运行程序时,看不到上次程序的数据,如果要将数据进行持久化的保存,我们可以使用文件。
2. 什么是文件?
硬盘或磁盘上的文件就叫做文件。
在程序设计中我们一般会谈两种文件:程序文件、数据文件(从文件功能的角度来分类)
- 程序文件:
包括程序的源程序文件,目标文件(windows环境后缀为.obj),可执行程序文件(windows环境后缀为.exe) - 数据文件:
文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件。
在以前各章所处理数据的输入输出都是以终端为对象的,即从终端的键盘输入数据,运行结果显示到显示器上。
其实有时候我们会把信息输出到磁盘上,当需要的时候再从磁盘上把数据读取到内存中使用,这里处理的就是磁盘上文件。
3. 如何标识文件?
⼀个文件要有⼀个唯一的文件标识,以便用户识别和引用。其实就是文件名。
文件名包含3部分:文件路径 + 文件名主干 + 文件后缀
例如:
c:\code\test.txt
为了方便起见,文件标识常被称为文件名。
4. 二进制文件和文本文件?
根据数据的组织形式,数据文件被称为文本文件或二进制文件。
数据在内存中以二进制的形式存储,如果不加转换的输出到外存的⽂件中,就是二进制文件。
如果要求在外存上以ASCII码的形式存储,则需要在存储前转换。以ASCII字符的形式存储的文件就是文本文件。
那么一个数据在文件中是如何存储的呢?
字符⼀律以ASCII形式存储,数值型数据既可以用ASCII码形式存储,也可以使用二进制形式存储。
以10000为例,如果以ASCII码的形式输出到磁盘,则磁盘中占用5个字节(每个字符⼀个字节),而二进制形式输出,则在磁盘上只占4个字节。

5. 文件的打开和关闭
5.1 流和标准流
5.1.1 流
流(Stream)是一个抽象的概念,用于表示数据的流动。流可以是输入流(Input Stream)或输出流(Output Stream),分别用于从某个源读取数据和向某个目标写入数据。
C程序针对文件、画面、键盘等的数据输入输出操作都是通过流操作的。
一般情况下,我们要想向流里写数据,或者从流中读取数据,都是要打开流,然后操作。
5.1.2 标准流
那为什么我们从键盘输⼊数据,向屏幕上输出数据,并没有打开流呢?
那是因为C语言程序在启动的时候,默认打开了3个流:
• stdin: 标准输入流,在大多数的环境中从键盘输入,
scanf函数就是从标准输⼊流中读取数据。
• stdout: 标准输出流,大多数的环境中输出至显示器界面,\,printf函数就是将信息输出到标准输出流中。
• stderr: 标准错误流,⼤多数环境中输出到显示器界面。
这是默认打开了这三个流,我们使用scanf、printf等函数就可以直接进行输入输出操作的。
stdin、stdout、stderr三个流的类型是:FILE*,通常称为文件指针。
C语言中,就是通过 FILE* 的文件指针来维护流的各种操作的。
5.2 文件指针
缓冲文件系统中,关键的概念是文件类型指针,简称文件指针。
每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件的名字,文件状态及文件当前的位置等)。这些信息是保存在⼀个结构体变量中的。该结构体类型是由系统声明的,取名FILE.
例如vs2013下stdio.h头文件中包含的文件有如下的定义
struct _iobuf
{char* _ptr;int _cnt;char* _base;int _flag;int _file;int _charbuf;int _bufsiz;char* _tmpfname;
};
typedef struct _iobuf FILE;
不同的编译器对文件的定义方式不同,但大同小异。
每当打开一个文件的时候,系统会根据文件的情况自动创建⼀个FILE结构的变量,并填充其中的信息,使用者不必关心细节。
一般都是通过一个FILE的指针来维护这个FILE结构的变量,这样使用起来更加放便。
下面我们可以创建一个FILE*的指针变量:
FILE* pf; //⽂件指针变量
定义的pf是⼀个指向FILE类型数据的指针变量。可以使pf指向某个文件的文件信息区(是一个结构体变量)。通过该文件信息区中的信息就能够访问该文件。也就是说,通过文件指针变量能够间接找到与它关联的文件。
比如:

5.3 文件的打开和关闭
文件在读写之前首先应当打开文件,使用结束之后应当关闭文件
在编写程序的时候,在打开文件的同时,都会返回一个FILE*的指针指向该文件,相当于建立了文件和指针的关系。
ANSIC规定使用fopen函数来打开文件, fclose函数来关闭文件。
//打开文件
FILE* fopen(const char * filename, const char * mode);//第一个参数为文件名,第二个参数为文件的打开方式
//关闭文件
int fclose(FILE * stream);
mode表示文件的打开模式,下面都是文件的打开模式:
| 文件使用方式 | 含义 | 如果指定文件不存在 |
|---|---|---|
| “r“(只读) | 为了输入数据打开一个已经存在的文件 | 出错 |
| “w”(只写) | 为了输出数据,打开一个文本文件 | 建立一个新的文件 |
| “a”(追加) | 向文本文件尾添加数据 | 建立一个新的文件 |
| “rb”(只读) | 为了输入数据,打开一个二进制⽂件 | 出错 |
| “wb”(只写) | 为了输出数据,打开一个二进制⽂件 | 建立一个新的文件 |
| “ab”(追加) | 向一个二进制⽂件尾添加数据 | 建立一个新的文件 |
| “r+”(读写) | 为了读和写,打开一个文本文件 | 出错 |
| “w+”(读写) | 为了读和写,建立一个新的文件 | 建立一个新的文件 |
| “a+”(读写) | 打开一个文件,在文件尾进行读写 | 建立一个新的文件 |
| “rb+”(读写) | 为了读和写打开一个⼆进制文件 | 出错 |
| “wb+”(读写) | 为了读和写,新建一个新的二进制文件 | 建立一个新的文件 |
| “ab+”(读写) | 打开一个⼆进制文件,在文件尾进行读和写 | 建立一个新的文件 |
代码实现:
int main()
{//打开文件//打开文件成功,返回有效的指针//打开失败,返回NULLFILE* pf = fopen("data.txt","w");if (pf == NULL){perror("fopen");return 1;}//写文件//关闭文件fclose(pf);pf = NULL; //关闭之后应将pf置为空,否则将会成为野指针return 0;
}
6. 文件的读写顺序
6.1 顺序读写函数
| 函数名 | 功能 | 适用于 |
|---|---|---|
| fgetc | 字符输⼊函数 | 所有输⼊流 |
| fputc | 字符输出函数 | 所有输出流 |
| fgets | 文本行输⼊函数 | 所有输⼊流 |
| fputs | 文本行输出函数 | 所有输出流 |
| fscanf | 格式化输⼊函数 | 所有输⼊流 |
| fprintf | 格式化输出函数 | 所有输出流 |
| fread | ⼆进制输⼊ | 文件 |
| fwrite | ⼆进制输出 | 文件 |
举例fputc:
int main()
{FILE* pf = fopen("data.txt","w");if (pf == NULL){perror("fopen");return 1;}//写文件fputc('a', pf);fputc('b', pf);fputc('c', pf);fputc('d', pf);fputc('e', pf);//关闭文件fclose(pf);pf = NULL;return 0;
}
我们可以看到data.txt文件中多了abcde

举例fgutc:
int main()
{FILE* pf = fopen("data.txt", "r");if (pf == NULL){perror("fopen");return 1;}//读文件int ch = fgetc(pf);printf("%c", ch);ch = fgetc(pf);printf("%c", ch);ch = fgetc(pf);printf("%c", ch);ch = fgetc(pf);printf("%c", ch);ch = fgetc(pf);printf("%c", ch);//关闭文件fclose(pf);pf = NULL;return 0;
}
程序的运行结果:

上面说的适用于所有输入流⼀般指适用于标准输入流和其他输入流(如文件输入流);所有输出流⼀般指适用于标准输出流和其他输出流(如文件输出流)。
int main()
{int ch = fgetc(stdin);//从键盘上(标准输入流)上读取fputc(ch,stdout); //将字符输出(写)到屏幕(标准输出流)return 0;
}
6.2 对比一组函数
scanf/printf:针对标准输入流/标准输出流的 格式化 输入/输出函数
fscanf/fprintf:针对所有输入流/所有输出流的 格式化 输入/输出函数
sscanf/sprintf:将格式化的数据转化成字符串/从字符串中提取格式化数据
sprinft: 从字符串中提取格式化的数据(将字符串转化为格式化数据)
sscanf: 将格式化的数据写到字符串中(将格式化的数据转化成字符串)
7. 文件的随机读写
7.1 fseek
根据文件指针的位置和偏移量来定位文件指针(文件内容的光标)。
int fseek ( FILE * stream, long int offset, int origin );//参数分别是文件指针,偏移量(可以是正值,也可以是负值),起始位置
代码演示:
int main()
{//1.打开文件FILE* pf = fopen("data.txt", "r");if (pf == NULL){perror("fopen");return 1;}//2.读文件//fseekint ch = 0;fseek(pf, 4, SEEK_SET);//SEEK_SET起始位置ch = fgetc(pf);printf("%c\n", ch);fseek(pf, 2, SEEK_CUR);//SEEK_CUR当前位置ch = fgetc(pf);printf("%c\n", ch);fseek(pf, -2, SEEK_END);//SEEK_END文件末尾ch = fgetc(pf);printf("%c\n", ch);//3.关闭文件fclose(pf);pf = NULL;return 0;
}

输出结果:

7.2 ftell
返回文件指针相对于起始位置的偏移量
long int ftell ( FILE * stream );
示例:
int main()
{//1.打开文件FILE* pf = fopen("data.txt", "r");if (pf == NULL){perror("fopen");return 1;}//2.读文件int ch = 0;ch = fgetc(pf);printf("%c\n", ch);//ach = fgetc(pf);printf("%c\n", ch);//b//fseekfseek(pf, -2, SEEK_END);//SEEK_END文件末尾ch = fgetc(pf);printf("%c\n", ch);//e//输出文件指针相较于起始位置的偏移量printf("%d\n",ftell(pf));//3.关闭文件fclose(pf);pf = NULL;return 0;
}
输出结果:

7.3 rewind
让文件指针的位置回到文件的起始位置。
void rewind ( FILE * stream );
代码演示:
int main()
{//1.打开文件FILE* pf = fopen("data.txt", "r");if (pf == NULL){perror("fopen");return 1;}//2.读文件int ch = 0;ch = fgetc(pf);printf("%c\n", ch);//ach = fgetc(pf);printf("%c\n", ch);//b//fseekfseek(pf, -2, SEEK_END);//SEEK_END文件末尾ch = fgetc(pf);printf("%c\n", ch);//e//输出文件指针相较于起始位置的偏移量printf("%d\n",ftell(pf)); //5rewind(pf);ch = fgetc(pf);printf("%c\n", ch);//3.关闭文件fclose(pf);pf = NULL;return 0;
}
输出结果:

可以看到光标又指回了a
8. 文件读取结束的判定
8.1 被错误使用的feof
EOF - end of file:文件结束的标志
所以大家都会认为feof函数是用来判断文件是否结束的,但是其实并不是。
feof的作用: 当文件读取结束的时候,判断读取结束的原因是不是:遇到文件结尾结束
在读取文件的过程中,有可能读取文件结束,结束的原因是:
- 遇到文件结尾
- 遇到错误了
1.文本文件读取是否结束,判断返回值是否为 EOF ( fgetc ),或者 NULL ( fgets )。
例如:
fgetc判断是否为 EOF 。fgets判断返回值是否为 NULL .
2.二进制文件的读取结束判断,判断返回值是否小于实际要读的个数。
例如:
fread判断返回值是否小于实际要读的个数。
9. 文件缓冲区
ANSIC 标准采用缓冲文件系统处理数据文件的,所谓的缓冲文件系统是指系统自动地在内存中为程序中的每一个正在使用的文件开辟一块文件缓冲区。从内存向磁盘输出数据先会送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的大小根据c编译系统决定的。

代码验证:
#include <stdio.h>
#include <windows.h>
int main()
{FILE* pf = fopen("data.txt","w");fputs("abcdef",pf); //先将abcdef放在输出缓冲区printf("睡眠10秒,打开data.txt发现没有内容\n");Sleep(10000);printf("刷新缓冲区\n");fflush(pf); //刷新缓冲区时,才将输出缓冲区的数据写到文件(磁盘)printf("再睡眠10秒,再次打开data.txt文件,文件有内容了\n");Sleep(10000);fclose(pf);//注:fclose在关闭文件的时候,也会刷新缓冲区 pf = NULL;return 0;
}
结论:
因为有缓冲区的存在,C语言在操作文件的时候,需要做刷新缓冲区或者在文件操作结束的时候关闭文件。
如果这篇文章对你有帮助,记得点赞,评论+收藏 ,最后别忘了关注作者,作者将带领你探索更多关于c语言方面的问题。
相关文章:
【c语言】文件操作详解 - 从打开到关闭
文章目录 1. 为什么使用文件?2. 什么是文件?3. 如何标识文件?4. 二进制文件和文本文件?5. 文件的打开和关闭5.1 流和标准流5.1.1 流5.1.2 标准流 5.2 文件指针5.3 文件的打开和关闭 6. 文件的读写顺序6.1 顺序读写函数6.2 对比一组…...
Flink Sink的使用
经过一系列Transformation转换操作后,最后一定要调用Sink操作,才会形成一个完整的DataFlow拓扑。只有调用了Sink操作,才会产生最终的计算结果,这些数据可以写入到的文件、输出到指定的网络端口、消息中间件、外部的文件系统或者是…...
pcl::PointCloud<PointType>::Ptr extractedCloud; 尖括号里的值表示什么含义?
在C中,pcl::PointCloud<PointType>::Ptr是一种智能指针,它是Point Cloud Library (PCL)中用于管理pcl::PointCloud对象的智能指针类型。这里的<pcl::PointCloud<PointType>::Ptr>尖括号里的值表示智能指针所指向的对象类型。 让我们分…...
《基于FPGA的便携式PWM方波信号发生器》论文分析(三)——数码管稳定显示与系统调试
一、论文概述 基于FPGA的便携式PWM方波信号发生器是一篇由任青颖、庹忠曜、黄洵桢、李智禺和张贤宇 等人发表的一篇期刊论文。该论文主要研究了一种新型的信号发生器,旨在解决传统PWM信号发生器在移动设备信号调控中存在的精准度低和便携性差的问题 。其基于现场可编…...
VsCode 插件推荐(个人常用)
VsCode 插件推荐(个人常用)...
路由策略与路由控制实验
AR1、AR2、AR3在互联接口、Loopback0接口上激活OSPF。AR3、AR4属于IS-IS Area 49.0001,这两者都是Level-1路由器,AR3、AR4的系统ID采用0000.0000.000x格式,其中x为设备编号 AR1上存在三个业务网段A、B、C(分别用Loopback1、2、3接…...
训练的decoder模型文本长度不一致,一般设置为多大合适,需要覆盖最长的文本长度么
在训练解码器模型时,文本长度不一致是常见的情况,需要根据任务的特性和数据集的长度分布来设置合理的最大长度 (max_length)。以下是一些指导原则,帮助你设置合适的最大长度: 1. 是否需要覆盖最长文本长度 覆盖最长文本长度: 如果任务对完整性要求很高(例如生成数学公式、…...
过滤条件包含 OR 谓词,如何进行查询优化——OceanBase SQL 优化实践
这篇博客涉及两个点,一个是 “OR Expansion 改写”,另一个是 “基于代价的改写”。 背景 在写SQL查询时,难以避免在过滤条件中使用 OR 谓词,但其往往会导致索引利用效率下降的问题 。本文将分享如何通过查询改写的2种方式进行优化…...
通过异步使用消息队列优化秒杀
通过异步使用消息队列优化秒杀 同步秒杀流程异步优化秒杀异步秒杀流程基于lua脚本保证Redis操作原子性代码实现阻塞队列的缺点 同步秒杀流程 public Result seckillVoucher(Long voucherId) throws InterruptedException {SeckillVoucher seckillVoucher iSeckillVoucherServi…...
AI产业告别“独奏”时代,“天翼云息壤杯”高校AI大赛奏响产学研“交响乐”
文 | 智能相对论 作者 | 陈泊丞 人工智能产业正在从“独奏”时代进入“大合奏”时代。 在早期的AI发展阶段,AI应用主要集中在少数几个领域,如语音识别、图像处理等。这些领域的研究和开发工作往往由少数几家公司或研究机构即可独立完成,犹…...
Hot100 - 字母异位词分组
Hot100 - 字母异位词分组 最佳思路:排序 时间复杂度: O(nmlogm),其中 n 为 strs 数组的长度,m 为每个字符串的长度。 代码: class Solution {public List<List<String>> groupAnagrams(String[] strs) …...
力扣hot100-->排序
排序 1. 56. 合并区间 中等 以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] [starti, endi] 。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间 。 示例 1: 输…...
【VRChat 全身动捕】VIVE 手柄改 tracker 定位器教程,低成本光学动捕解决方案(持续更新中2024.11.26)
更新 0.0.1(2024/11/26): 1.解决了内建蓝牙无法识别、“steamVR 蓝牙不可用” 的解决方案 2.解决了 tracker 虽然建立了连接但是在 steamVR 界面上看不到的问题 3.解决了 VIVE 基站1.0 无法被蓝牙识别 && 无法被 steamVR 搜索到 &…...
【Nginx】核心概念与安装配置解释
文章目录 1. 概述2. 核心概念2.1.Http服务器2.2.反向代理2.3. 负载均衡 3. 安装与配置3.1.安装3.2.配置文件解释3.2.1.全局配置块3.2.2.HTTP 配置块3.2.3.Server 块3.2.4.Location 块3.2.5.upstream3.2.6. mine.type文件 3.3.多虚拟主机配置 4. 总结 1. 概述 Nginx是我们常用的…...
Qt界面篇:QMessageBox高级用法
1、演示效果 2、用法注意 2.1 设置图标 用于显示实际图标的pixmap取决于当前的GUI样式。也可以通过设置icon pixmap属性为图标设置自定义pixmap。 QMessageBox::Icon icon(...
【二叉树】【2.1遍历二叉树】【刷题笔记】【灵神题单】
关注二叉树的三个问题: 什么情况适合自顶向下?什么时候适合用自底向上?一般来说,DFS的递归边界是空节点,什么情况下要额外把叶子节点作为递归边界?在什么情况下,DFS需要有返回值?什…...
Mongo数据库 --- Mongo Pipeline
Mongo数据库 --- Mongo Pipeline 什么是Mongo PipelineMongo Pipeline常用的几个StageExplanation with example:MongoDB $matchMongoDB $projectMongoDB $groupMongoDB $unwindMongoDB $countMongoDB $addFields Some Query Examples在C#中使用Aggreagtion Pipeline**方法一: …...
Adobe Illustrator 2024 安装教程与下载分享
介绍一下 下载直接看文章末尾 Adobe Illustrator 是一款由Adobe Systems开发的矢量图形编辑软件。它广泛应用于创建和编辑矢量图形、插图、徽标、图标、排版和广告等领域。以下是Adobe Illustrator的一些主要特点和功能: 矢量绘图:Illustrator使用矢量…...
javax.xml.ws.soap.SOAPFaultException: ZONE_OFFSET
javax.xml.ws.soap.SOAPFaultException 表示 SOAP 调用过程中发生了错误,并且服务端返回了一个 SOAP Fault。 错误信息中提到的 ZONE_OFFSET 可能指的是时区偏移量。在日期和时间处理中,时区偏移量是指格林威治标准时间 (GMT) 的偏移量。如果服务期望特…...
常用的数据结构
队列(FIFO) 栈(LIFO) 链表 hash表 hash冲突处理 开放式寻址 线性探测 表示依次检查索引为 hash(key) + 1、hash(key) + 2 ... 的位置。i 是冲突后的探查步数。公式:hash(i) = (hash(key) + i) % TableSize二次探查 规则:冲突后探查的步长是平方递增的,例如,检查位置为 hash…...
使用docker在3台服务器上搭建基于redis 6.x的一主两从三台均是哨兵模式
一、环境及版本说明 如果服务器已经安装了docker,则忽略此步骤,如果没有安装,则可以按照一下方式安装: 1. 在线安装(有互联网环境): 请看我这篇文章 传送阵>> 点我查看 2. 离线安装(内网环境):请看我这篇文章 传送阵>> 点我查看 说明:假设每台服务器已…...
(十)学生端搭建
本次旨在将之前的已完成的部分功能进行拼装到学生端,同时完善学生端的构建。本次工作主要包括: 1.学生端整体界面布局 2.模拟考场与部分个人画像流程的串联 3.整体学生端逻辑 一、学生端 在主界面可以选择自己的用户角色 选择学生则进入学生登录界面…...
Unity3D中Gfx.WaitForPresent优化方案
前言 在Unity中,Gfx.WaitForPresent占用CPU过高通常表示主线程在等待GPU完成渲染(即CPU被阻塞),这表明存在GPU瓶颈或垂直同步/帧率设置问题。以下是系统的优化方案: 对惹,这里有一个游戏开发交流小组&…...
Vue3 + Element Plus + TypeScript中el-transfer穿梭框组件使用详解及示例
使用详解 Element Plus 的 el-transfer 组件是一个强大的穿梭框组件,常用于在两个集合之间进行数据转移,如权限分配、数据选择等场景。下面我将详细介绍其用法并提供一个完整示例。 核心特性与用法 基本属性 v-model:绑定右侧列表的值&…...
C++中string流知识详解和示例
一、概览与类体系 C 提供三种基于内存字符串的流,定义在 <sstream> 中: std::istringstream:输入流,从已有字符串中读取并解析。std::ostringstream:输出流,向内部缓冲区写入内容,最终取…...
【HTTP三个基础问题】
面试官您好!HTTP是超文本传输协议,是互联网上客户端和服务器之间传输超文本数据(比如文字、图片、音频、视频等)的核心协议,当前互联网应用最广泛的版本是HTTP1.1,它基于经典的C/S模型,也就是客…...
使用 SymPy 进行向量和矩阵的高级操作
在科学计算和工程领域,向量和矩阵操作是解决问题的核心技能之一。Python 的 SymPy 库提供了强大的符号计算功能,能够高效地处理向量和矩阵的各种操作。本文将深入探讨如何使用 SymPy 进行向量和矩阵的创建、合并以及维度拓展等操作,并通过具体…...
Angular微前端架构:Module Federation + ngx-build-plus (Webpack)
以下是一个完整的 Angular 微前端示例,其中使用的是 Module Federation 和 npx-build-plus 实现了主应用(Shell)与子应用(Remote)的集成。 🛠️ 项目结构 angular-mf/ ├── shell-app/ # 主应用&…...
【生成模型】视频生成论文调研
工作清单 上游应用方向:控制、速度、时长、高动态、多主体驱动 类型工作基础模型WAN / WAN-VACE / HunyuanVideo控制条件轨迹控制ATI~镜头控制ReCamMaster~多主体驱动Phantom~音频驱动Let Them Talk: Audio-Driven Multi-Person Conversational Video Generation速…...
技术栈RabbitMq的介绍和使用
目录 1. 什么是消息队列?2. 消息队列的优点3. RabbitMQ 消息队列概述4. RabbitMQ 安装5. Exchange 四种类型5.1 direct 精准匹配5.2 fanout 广播5.3 topic 正则匹配 6. RabbitMQ 队列模式6.1 简单队列模式6.2 工作队列模式6.3 发布/订阅模式6.4 路由模式6.5 主题模式…...
