1. 输入输出初步认识
文件的读写,标准上称之为输入流(input stream)和输出流(output stream)。
输入流的数据会首先被暂存到一个叫缓冲区(buffer)的内存区域。
缓冲区的作用:
- 检查安全的一种机制;
- 一部分数据打包放在缓冲区中,用于批量处理,提高数据处理的效率。
标准输入(stdin):指从键盘输入数据到操作系统中,存储这些数据在标准输入关联的缓冲区中,等待被程序读取。
标准输出(stdout):可以把数据发送到外部,例如输出数据到控制台、文件等。
2. 复习scanf_s
根据前面学过的章节,在对字符串进行修改时,使用带_s的函数,可有效避免缓冲区溢出的问题。
接下来是一些复习。
需要注意的是,只要且只有修改字符或字符串时,必须加第三个参数,传入想要输入的长度,避免缓冲区溢出。对于输入数字,不需要加第三个参数。
输入字符串:
1 |
|
输入字符:
1 | char ch; |
输入整数:
1 | int age; |
3. scanf_s的返回值
一个例子:
1 | uint32_t number; |
EOF是“end of file”的缩写,表示没有更多数据可供读取,其值通常为-1。
scanf_s的返回值为一个整数,只有当返回值为1的时候,代表输入被成功读取。
4. stream流的概述
以在文本编辑器中写入数据为例,没保存之前,所有数据都被暂存在内存的缓冲区中,如果没保存时退出程序,这些临时数据就会丢失。点击保存后,内存缓冲区中的数据就会被转移到硬盘中,变成永久性数据。
流(Stream)包含以下几种:
- 文件流:用于读取与写入在磁盘上的文件,这是本章的重点;
- 标准I/O流:包括
stdin(默认连接到键盘,用于程序输入,例如scanf_s())、stdout(默认连接到控制台或者屏幕,用于程序输出,例如printf())、stderr(默认也连接到控制台或屏幕,专门输出错误信息和警告,使其能够被区分开来或者是重定向到不同的目的地); - 管道流:用于进程之间的通信(IPC),允许一个进程的输出成为另一个进程的输入。例如
popen(); - 内存流:允许将流与内存缓冲区关联,使得用于可以向内存中读写数据,就像操作文件一样。例如
fmemopen(); - 网络流:套接字(Sockets);
- 设备流:特殊文件或是设备,例如打印机。
在C语言中,使用FILE*类型来定义流指针,例如:FILE* stream。
5. 读取r模式(fopen_s、fgetc、fgets、fclose)
使用FILE*可以创建文件流指针,初始化为NULL。
打开文件时为了保证缓冲区不溢出,使用fopen_s()函数,传入三个参数,第一个是文件流的二级指针,第二个是文件路径,第三个是文件打开模式。
Windows下通常使用
\作为路径分隔符,例如C:\Users\HC\Desktop\file.txt。但在输入文件路径时,反斜杠\是转义符,后面需要再加一个反斜杠,使用双反斜杠\\,转义之后才能得到反斜杠,这才是正确的路径,例如C:\\Users\\HC\\Desktop\\file.txt。
5.1 按行读取
读取单行文件时使用fgets()函数,传入三个参数,第一个是定义的内存缓冲区,第二个是缓冲区长度,第三个是打开的文件流。
文件操作完成后,需要调用fclose()关闭文件流,传入想要关闭的文件流即可,避免文件流被一直占用。
1 |
|
值得注意的是,微软的编译器非常智能,如果在打开文件时不对可能出现的错误类型做判断处理,后续使用到文件流的函数会报警告:"file_stream" 可能是 "0": 这不符合函数 "fgets" 的规范。。
此时,虽然程序在文件打开没有出错的情况下可以正常运行,但一旦文件打开出错,例如文件路径错误,文件异常,文件权限错误等,程序在Debug模式下会直接弹窗报错,Release模式下无法正常运行。
这时候就需要在打开文件时,对fopen_s()函数的返回值做判断和处理,保证文件正常打开。fopen_s()的返回值是一个errno_t类型的值,当这个值不为0,或者文件流仍为NULL时,说明文件没有被正常打开,此时使用perror()函数返回错误类型,并退出程序即可。
使用
EXIT_FAILURE宏定义需要引入标准库stdlib.h。
1 |
|
可以使用循环,调用fgets()函数读取一个文件中的所有内容,直到fgets()函数的返回值为NULL时,文件读取完毕。
当读取到的数据为NULL或EOF时,代表这个文件结束了。
1 | FILE* file_stream = NULL; // 文件流指针 |
由于读取的每一行中已经包含了换行符,因此打印缓冲区时候不需要加
\n。
5.2 按字符读取
fgetc()函数是按照字符读取文件,只需要传入一个文件流参数,返回的是读取到的字符。
准确来说,
fgetc()的返回值是int类型,对应的读取到的字符的ASCII码。
1 | FILE* file_stream = NULL; // 文件流指针 |
同样的,使用fgetc()函数读取字符,也可以使用循环读取整个文件。当读取到的字符为EOF时,文件读取完毕。
1 | FILE* file_stream = NULL; // 文件流指针 |
由于文件中每一行结束时都包含换行符,也会被当成字符读取,因此输出的内容会与文件中的内容一样,包含换行。
5.3 同一个文件流多次读取
可以在同一个程序中分别使用fgets()与fgetc()通过行和字符读取整个文件。
需要注意的是,第一次读取完成后,文件流指针已经指向了文件末尾,导致第二次一开始读取到的就是文件末尾,无法正常读取整个文件。
此时需要调用rewind()函数来将文件流指针复位到文件开头。
在行读取结束之后,可以使用memset函数释放缓冲区,这个函数包含在头文件string.h中。
为了文件的安全性,只要涉及到与写文件相关的操作,关闭文件时也需要和打开文件时一样,对函数的返回值做判断和错误返回操作。读取文件时可写可不写。
1 |
|
6. 写入w模式(fputs、fputc、fprintf_s)
fopen_s函数中常用的模式和用途如下:
| mode | Access |
|---|---|
| “r” | 打开以便读取。如果文件不存在或找不到,fopen_s调用将失败。 |
| “w” | 打开用于写入的空文件。如果给定文件存在,则其内容会被销毁。 |
| “a” | 在文件末尾打开以写入(追加),在新数据写入到文件之前不移除文件末尾(EOF)标记。如果文件不存在,则创建文件。 |
| “r+” | 打开以便读取和写入。文件必须存在。 |
| “w+” | 打开用于读取和写入的空文件。如果文件存在,则其内容会被销毁。 |
| “a+” | 打开以进行读取和追加。追加操作包括在新数据写入文件之前移除EOF标记。写入完成后,EOF标记不会还原。如果文件不存在,则创建文件。 |
因此,使用w模式时,文件中原有的内容会被全部覆盖并替换为新内容。
打开与关闭文件的方式与读取文件时相同,模式改为“w”。
fputs()用于写入一行内容,传入要写入的字符串与文件流指针。
fputc()用于写入一个字符,传入要写入的字符与文件流指针。
fprintf()用于格式化写入,第一个参数是文件流指针,后面的参数与printf()相同。
1 | FILE* file_stream = NULL; // 文件流指针 |
实际上,调用写入函数写入文件时,也应该对返回值进行判断,防止出现在文件打开模式为读取并调用写入函数时,无任何报错,也没有成功写入的情况。
1 | // 写入行,并进行错误处理 |
如果无法写入,输出:Error writting file: Bad file descriptor。
7. ftell、fseek、rewind
在fopen_s()函数中使用“r+”模式,以便读取和写入。
ftell()用于获取当前的文件流指针的位置,需要传入文件流指针,返回long类型的数据。
fseek()用于将文件流指针移动到指定位置,需要传入文件流指针、相对第三个参数的偏移量,以字节为单位、开始添加偏移的位置。
一些常用偏移位置的常量宏定义:
| 常量 | 描述 |
|---|---|
SEEK_SET | 文件的开头 |
SEEK_CUR | 文件指针的当前位置 |
SEEK_END | 文件的末尾 |
rewind()用于将文件流指针复位到文件开始,只需传入文件流指针即可。
1 | FILE* file_stream = NULL; // 文件流指针 |
需要注意的是,Windows系统采用的换行符是CRLF(
\r\n),因此一行的字符数是可以看到的字符数+2。例如,如果第一行为
Hi,则真实读取文件时为Hi\r\n,因此读取完一行后,文件流指针的位置为4。
8. fscanf_s
fscanf_s函数用于从流中格式化读取数据。
因为是读取,打开文件模式选择“r”。
例如准备读取的文件内容为:
1 | Hello! 6666 5.34 H |
可以使用fscanf_s函数分别读取每一种类型,并把读取到的数据储存在变量或缓冲区中。
fscanf_s函数需要传入的参数与scanf_s类似,区别是需要在最前面加一个文件流指针参数。
可以对fscanf_s函数的返回值做一些判断,当返回值为1时,则读取成功,否则读取失败。
1 | FILE* file_stream = NULL; // 文件流指针 |
9. fprintf
fprintf用于将缓冲区的数据写入到文件中。
打开文件模式使用“w”,即覆盖式写入。
使用方式与printf类似,区别是需要在最前面加一个文件流指针参数。
1 | FILE* file_stream = NULL; // 文件流指针 |
10. ferror、feof、clearerr
这三个函数用来处理文件流中的错误,前两个是错误检查,第三个是清除错误。
ferror:检查文件流中或文件读写中是否有错误发生,如果有错误,返回一个非0值,如果没有错误,返回0;
feof:检查文件流指针是否到达了文件的末尾,如果文件流指针到达了文件的末尾,返回非0值,否则返回0;
clearerr:清除与文件流相关的错误和标志,确保文件流恢复到没有错误,文件流指针没有到达文件末尾的状态。
1 | FILE* file_stream = NULL; // 文件流指针 |
11. 抽离读写函数
可以将读取文件的功能抽离为单独的函数,简化主函数。
抽离出来的函数中,打开文件时的错误处理没有用perror,而是用了另一种企业中常用的更为标准的写法,将错误信息先储存在缓冲区中,再将其输出为标准错误。
strerror_s是安全版本的错误转换函数,将错误码errno转换为可读的错误消息;errno是全局错误码变量,记录最近一次系统调用的错误类型(如权限不足、文件不存在等)。
stderr是标准错误输出流,用于输出错误信息(与stdout分离)。
fprintf可以将错误信息格式化输出到标准错误输出流中。
exit是进程级退出,调用时直接终止整个程序,而return只能终止当前函数。此时打开文件遇到错误时,程序不应该继续执行。
strerror_s函数包含在头文件string.h中。
1 |
|
12. 追加a模式
“a”模式可以在一个文件的末尾追加内容,如果文件不存在,则创建文件。
使用“a”或“a+”模式打开文件时,所有写入操作均在文件的末尾进行。使用fseek或rewind可重新定位文件指针,但在执行任何写入操作前,文件指针将始终被移回文件末尾,以确保不会覆盖现有数据。
可以单独抽离出一个函数,用于在文件末尾追加指定内容。
由于追加包含了文件写入操作,文件关闭时应当做错误处理。
追加模式在追加内容时会自动换行,因此追加内容不需要手动写\n。
1 |
|
在主函数的最后可以加一个_fcloseall()函数,确保所有的流都已被关闭,这个函数的返回值是其在调用时关闭了多少个文件流,可以输出用于调试。
13. w模式清空
“w”模式在写入时会销毁并覆盖之前的文件,因此可以用这个模式实现清空文件的功能。
只要打开文件时使用的是“w”模式,不需要做其他任何操作,文件内容会被清空。
1 |
|
14. 企业实际案例(难):修改log,r+模式
这个案例用于将一个文件中第一次出现的特定字符串更新为另一个目标字符串。
打开文件时,使用“r+”模式可以实现文件的读取查找与修改更新。
更新文件的难度较大,这个操作涉及到文件的读和写,以及各种各样的判断。
这个例子的函数设计类似于企业标准写法,难度较大,可以作为学习参考。
Windows系统下的换行符是CRLF(
\r\n),在打开文件模式为“r+”时,读取文件时,程序自动在内存中将换行符由\r\n转换为\n,在写入文件时,程序自动在内存中将换行符由\n转换为\r\n。因此,使用
strlen得到读取文件缓冲区的字符串长度时,得到的长度会比实际长度少一个\r。
1 |
|
这个程序是针对一整行进行替换,替换部分字符的逻辑可以自行研究。
当替换字符串的长度大于源字符串的长度时,直接替换可能会导致下一行的内容被覆盖。
此程序简化了操作流程,在这种情况下直接返回错误,不进行替换。
对于这种情况,比较简单的处理方法是对于超出源字符串长度的部分直接截断,但这样可能会丢失一部分内容。
精细处理时,小文件可以先将剩余部分读取到内存中,替换后再写入剩余部分;大文件不方便全部读入内存,可以创建一个临时文件,逐行复制并替换,最后用临时文件替换原文件。
临时文件方案可以参考下一节。
15. 临时文件的方案
本节续上节的问题,使用创建临时文件的思路将一个文件中第一次出现的特定行内容替换为另一行目标内容。
用到了几个新的函数:
tmpnam_s:传入临时文件名缓冲区与缓冲区长度,函数会自动生成一个随机的文件名存放在缓冲区中,一般不会出现文件名重复的问题;
remove:传入想要删除的文件路径,返回值为0表示删除成功,返回非0值表示删除失败;
rename:传入两个文件路径,将第一个参数的文件重命名为第二个参数中的文件名,返回0表示重命名成功,返回非0值表示重命名失败。
1 |
|
此程序只会替换第一次出现目标字符串的行。
只要一行中出现目标字符串,就会将整行替换为新的字符串。
16. fflush简单掠过
fflush用于保存文件,传入一个文件流指针。调用这个函数时,缓冲区中的内容会被立刻写入磁盘。
例如在多线程保存文件或者立即保存错误日志的场景下,需要用到这个功能。
17. 游戏设置案例:bin二进制文件存储与读取,wb与rb模式的使用
读取和写入二进制数据(图片、音频等)一般用到fread与fwrite函数。
本案例演示一个简单的写入与读取游戏设置二进制文件的功能。
打开文件时,"wb"模式用来写入二进制文件,“rb”模式用来读取二进制文件。
fwrite函数传递四个参数:
ptr:指向要写入的数据的指针。size:要被写入的每个元素的大小,以字节为单位。nmemb:元素的个数,每个元素的大小为size字节。stream:指向FILE结构的指针。
返回值:如果成功,该函数返回一个size_t对象,表示元素的总数,该对象是一个整型数据类型。如果该数字与nmemb参数不同,则会显示一个错误。
fread函数传递四个参数:
ptr:数据的存储位置。size:要读取的每个元素的大小,以字节为单位。nmemb:元素的个数,每个元素的大小为size字节。stream:指向FILE结构的指针。
返回值:成功读取的元素总数会以size_t对象返回,size_t对象是一个整型数据类型。如果总数与nmemb参数不同,则可能发生了一个错误或者到达了文件末尾。
1 |
|
18. 复制文件
复制文件本质上就是将一个文件中的所有内容都读取出来,并写入到另一个文件中。
1 |
|
19. 第十一章结束语
多思考,多练习!
本文链接: https://hanqingjiang.com/2026/02/03/20260203_C_file/
版权声明: 本作品采用 CC BY-NC-SA 4.0 进行许可。转载请注明出处!
