C语言教程-16_1-文件与文件操作
前置知识:
- 指针
- 字符串
文件I/O流
在3_2
章提到流
的概念,文件流就是将磁盘中的文件读写抽象成了流。
文件流可以分为2类,一类是常规的文本流,其中的数据是供人阅读的文本;
另一类就是二进制流,其中的数据供程序读取使用。
我们这里学习标准C读写文件的方法,在打开文件的时候,可以选择使用文本模式
还是二进制模式
进行读写,两者差异其实并不大,一般仅体现在换行符的处理上。
打开文件
FILE类型
首先,在打开一个文件之后,我们势必需要使用某种对象来代表这个文件,并进行读写操作,亦即需要使用某个对象来保持文件的打开状态。
C语言使用 FILE
类型实现该功能,它是一个结构体,定义在stdio.h
中:
1 | struct _iobuf { |
我们无需记忆该结构体的实现细节,对外而言,只需要使用一个FILE*
即可记录一个文件的状态,即使用一个指向FILE
类型的指针来持有一个文件。
在打开一个文件后,后续的读写操作均围绕该FILE*
变量展开即可,均封装在C标准提供的若干函数中。
fopen函数
打开一个文件对象(亦即FILE *
指针指向的对象)需要2个信息:
- 文件路径:文件路径包括了文件名,分为2类,一类是绝对路径,另一类是相对路径
- 文件访问标记:以何种模式打开文件,包括是否可写、文本模式、追加还是从头开始等
C标准提供了fopen
函数来打开文件,上述两个信息作为2个字符串参数进行提供:
1 | FILE *fopen( const char *filename, const char *mode ); // C99前 |
若成功,返回指向新文件流的指针;错误时,返回空指针NULL。
文件路径
其中,filename
是一个字符指针,指向了要打开的文件路径字符串,有2种方式:
- 绝对路径:即从根目录开始写起的路径,可以直接唯一定位文件,例如:
- Linux下:
"/home/username/C/test.txt"
- Windows下:
"D:/data/code/C/test.txt"
- Linux下:
- 相对路径:并非从根目录开始写起,而是以工作路径为基础,在其之上进行相对定位的路径,例如:
- Linux下:
test.txt
,此时如果工作路径是/home/username/C/
,则文件的实际路径为"/home/username/C/test.txt"
- Windows下:
"./C/text.txt"
,此时如果工作路径是"D:/data/code/"
,则文件的实际路径为"D:/data/code/C/test.txt"
- 工作路径:工作路径是程序在执行时所处的路径,一般情况下是程序可执行文件所在的路径
- Linux下:
文件访问标记
mode
(文件访问标记)同样是一个字符指针,指向了表示文件访问标记的字符串,fopen使用如下几种访问标记:
文件访问模式字符串 | 含义 | 解释 | 若文件已存在的动作 | 若文件不存在的动作 |
---|---|---|---|---|
“r” | 读 | 打开文件以读取 | 从头读 | 打开失败 |
“w” | 写 | 创建文件以写入 | 销毁内容 | 创建新文件 |
“a” | 追加 | 追加到文件 | 写到结尾 | 创建新文件 |
“r+” | 读扩展 | 打开文件以读/写 | 从头读 | 错误 |
“w+” | 写扩展 | 创建文件以读/写 | 销毁内容 | 创建新文件 |
“a+” | 追加扩展 | 打开文件以读/写 | 写到结尾 | 创建新文件 |
如果模式不是以上所列字符串之一,则其行为未定义,不过一些实现会定义额外支持的模式(例如Windows)
此外,在上述模式的基础上可以加上"b"
标记来以二进制模式打开文件,例如"rb"、"wb+"
等,该标记在POSIX上无效果,而在Windows上会禁用换行符的特殊处理(如下“两种读写模式的区别”所述)。
两种读写模式的区别
现在来解释文本模式
和二进制模式
的区别。
两种操作系统的换行符区别
首先需要了解Linux和Windows的换行符区别。
- Linux下的文件换行符就是一个
\n
即可(即0x0a
),也就是所谓的LF
; - 而Windows下则是两个字符
\r\n
代表换行,如果以十六进制表示就是0x0d0x0a
,也就是所谓的CRLF
Windows的CRLF占用2个字节,在文本模式时需要进行转换。
文本模式和二进制模式
文本模式这样处理数据:
- 写入文件时,将缓冲区中的
\n
转换为\r\n
再写入文件 - 从文件读取时,将文件中的
\r\n
转换为\n
,再写入缓冲区
而二进制模式则将\r\n
视为普通的两个字节,不进行转换,直接写入文件或缓冲区
打开文件举例
1 |
|
读写文件内容
读写文件有若干函数可以选择,分为“直接读写文件”、“无格式输入/输出”、“有格式输入/输出”
直接读写文件
fread函数-从文件读取若干数据
1 | size_t fread(void *buffer, size_t size, size_t count,FILE * stream); |
fread
函数从输入流 stream
读取至多 count
个对象到数组 buffer
中,每个对象的大小是 size
个字节。
其中:
- buffer:指向要读取的数组中首个对象的指针,或者说目的数组/缓冲区
- size:要读取的每个对象的大小,例如读取若干个int对象,size的值就是
sizeof(int)
- count:要读取的对象数,例如读取4个int对象,count的值就是4
- stream:读取来源的输入文件流,即从哪个文件读取
- 返回值:成功读取的对象数,若出现错误或文件尾条件,则可能小于
count
例如从文件中读取10个字符到buffer中:
1 |
|
fwrite函数-向文件写入若干数据
1 | size_t fwrite(const void *buffer, size_t size, size_t count, FILE * stream); |
fwrite
函数从数组 buffer
写入 count
个对象到输出流 stream
中,每个对象的大小是 size
个字节。
其中:
- buffer:指向数组中要被写入的首个对象的指针,或者说源数组/缓冲区
- size:要写入的每个对象的大小,例如写入若干个int对象,size的值就是
sizeof(int)
- count:要写入的对象数,例如写入4个int对象,count的值就是4
- stream:写入目的的输出文件流,即向哪个文件写入
- 返回值:成功写入的对象数,若出现错误或文件尾条件,则可能小于
count
例如将buffer中的字符串写入到文件中:
1 |
|
无格式输入/输出
fgetc和fputc
这两个函数读写单个字符
1 | int fgetc(FILE * stream); |
- fgetc从文件流
stream
中读取一个字符,作为返回值返回,失败则返回EOF
- fputc向文件流
stream
中写入字符ch
,成功时返回该字符ch
,失败则返回EOF
fgets和fputs
这两个函数读写字符串
1 | char *fgets(char *str, int count, FILE * stream); |
- fgets从给定文件流
stream
读取最多count-1
个字符到缓冲区str
中,然后写入一个空字符,读取时遇到文件尾或遇到换行符则停止,后一种情况下换行符也被读取到str
中。成功返回str
,失败则返回空指针 - fputs将以NULL结尾的字符串
str
写入到输出文件流stream
,不写入终止空字符。成功返回非负值(该值含义由实现定义),失败则返回EOF
有格式输入/输出
fscanf和fprintf
几乎和scanf和printf一模一样,唯一的区别就是:scanf和printf从 stdin
和 stdout
流输入输出,而fscanf和fprintf从指定的文件流输入输出。
1 | int fscanf(FILE * stream, const char *format, ...); |
这两个函数除了需要把操作的文件流作为第一个参数传入,其他方面和printf与scanf无异,故省略之
上述函数,读者根据自己的实际情况选择使用即可
文件定位
上述的文件读写都是顺序访问,我们有时还需要对文件进行随机访问,此时就需要在文件中进行定位。
ftell和fseek
这两个函数用于获取并修改当前文件位置。
首先了解文件位置指示器
:即当前文件指针所指向的文件内容位置距离文件开始处的字节数
ftell
函数返回流 stream
的文件位置指示器:
1 | long ftell(FILE * stream); |
其中:
- stream:要检验的文件流
- 返回值:成功时为文件指示器,失败时为
-1L
- 若流以二进制模式打开,则由此函数获得的值是从文件开始的字节数;
- 若流以文本模式打开,则由此函数返回的值未指定,且仅若作为
fseek()
的输入才有意义
示例:
1 |
|
ftell
用于获取当前文件的文件位置(指示器),而 fseek
用于设置当前文件的文件位置(指示器)
fseek
函数设置文件流 stream
的文件位置指示器为 offset
所指向的值:
1 | int fseek(FILE* stream, long offset, int origin); |
其中:
- stream:要修改的文件流
- offset:相对 origin 迁移的字符数
- origin:offset需要加上的位置,值为如下之一:SEEK_SET、SEEK_CUR、SEEK_END(只需要记住宏名即可),分别指示文件首、当前位置和文件尾
- 成功时返回
0
,否则为非零
需要注意:
- 若
stream
以二进制模式打开:则新位置被设置为origin
之后的offset
字节。- 若 origin 为
SEEK_SET
,则是文件起始之后 - 若 origin 为
SEEK_CUR
,则是当前文件位置之后 - 若 origin 为
SEEK_END
,则是文件结尾之后 - 不要求二进制流支持 SEEK_END,尤其是当其输出中附加了空字节时
- 若 origin 为
- 若
stream
以文本模式打开:则offset
的值仅支持零(可用于任何 origin)和先前在关联到同一个文件的流上对ftell
的调用的返回值(仅可用于SEEK_SET
)。 - 除了更改文件位置指示器,
fseek
还撤销ungetc
的效果并清除文件尾状态。 - 若发生读或写错误,则设置流的错误指示器(
ferror
)而不影响文件位置。
示例:
1 |
|
fgetpos和fsetpos
另一种获取并修改文件位置指示器的方法是使用 fgetpos
和 fsetpos
。
这两个函数使用一个新的类型 fpos_t
,该类型足以唯一指定文件的位置和多字节剖析状态,无需关心其具体实现。
fgetpos
获得文件流 stream
的文件位置指示器和当前分析状态(若存在),并将它们存储于 pos
所指向的对象:
1 | int fgetpos(FILE *stream, fpos_t *pos); |
其中:
- stream:要检验的文件流
- pos:指向用于存储获取到的文件位置指示器的
fpos_t
对象的指针 - 成功时返回
0
,否则返回非零值 - 存储的值仅在作为 fsetpos 的输入的情况有意义
fsetpos
设置文件流 stream
的文件位置指示器和多字节分析状态(若存在)。
1 | int fsetpos(FILE *stream, const fpos_t *pos); |
其中:
- stream:要修改的文件流
- pos:指向
pos_t
对象的指针,用作文件位置指示器的新值 - 成功时返回
0
,否则返回非零值
注意: - 在寻位到宽流的非结尾位置后,下个对任何输出函数的调用可能使剩下的文件内容未定义,例如通过输出不同长度的多字节序列。(例如定位到UTF8编码字符的第二个字符时)
rewind
rewind
函数移动文件位置指示器到给定文件流的起始:
1 | void rewind(FILE *stream); |
该函数等价于调用:
1 | fseek(stream, 0, SEEK_SET); |
但是它会清除文件尾和错误指示器。
另外,此函数丢弃任何来自先前对 ungetc
调用的结果。
对文件的操作
删除文件
remove
函数删除 pathname
所标识的文件:
1 | int remove(const char *pathname); |
其中:
- pathname:指向空终止字符串的指针,字符串含标识待删除文件的路径
- 成功时返回
0
,错误时返回非零值
若当前有任何进程打开了此文件,则此函数行为是实现定义的(此处略)。
重命名文件
rename
函数更改文件的文件名:
1 | int rename(const char *old_filename, const char *new_filename); |
其中:
- old_filename:指向包含要重命名的文件的路径的空终止字符串的指针
- new_filename:指向包含文件新路径的空终止字符串的指针
- 成功时返回
0
,错误时返回非零值
若new_filename
存在,则行为是实现定义的。
错误处理
feof
feof
函数检查是否已抵达给定文件流的结尾:
1 | int feof(FILE *stream); |
若已抵达流尾则返回非零值,否则返回0
。
ferror
ferror
函数检查给定文件流的错误:
1 | int ferror(FILE *stream); |
若文件流已出现错误则为非零值,否则返回0
。
clearerr
clearerr
函数重置给定文件流的错误标志和 EOF
指示器:
1 | void clearerr(FILE *stream); |
本章包含了C语言中大部分常用的文件读写、文件定位、文件处理以及错误检查的操作方式,基本足够平时使用。 当然,POSIX中还提供了 `read` 和 `write` 系统调用,同样可以实现对文件的读写(使用文件描述符),不在本书讨论范围内。
参考资料:
——WAHAHA 2024.4.28
上一篇:C语言教程-15_2-存储期,作用域与链接
下一篇: