本节主要讨论如何使用C语言读写文本文件。
在C语言里,操作一个文件的过程分为如下四步:①定义文件指针;②使用fopen()函数打开文件;③进行文件读写操作;④关闭文件。
版权声明
本文可以在互联网上自由转载,但必须:注明出处(作者:海洋饼干叔叔)并包含指向本页面的链接。
本文不可以以纸质出版为目的进行改编、摘抄。
我们通过下述示例来介绍上述过程。
1 | //Project - CreateSquareTable |
在作者的计算机上,上述程序的执行结果为:
1 | Current working directory: D:\C2Cpp\C20_FileIO\build-CreateSquareTable-Desktop_Qt_5_14_1_MinGW_64_bit-Debug |
除此之外,程序还在当前工作目录创建了一个名为SquareTable.txt的新文件,在资源管理器/文件管理器中找到这个文件并用记事本打开,可见其内容如下(前10行),这是一个平方值表。
1 | N N^2 |
🚩第6 ~ 8行:使用getcwd()函数获取程序的当前工作路径(Current Working Directory)并打印至屏幕。该函数由第3行的unistd.h头文件引入,其原型如下:
1 | char* getcwd(char* buf, int size); |
参数buf应指向预分配好的缓冲区(字符数组),size则为该缓冲区的大小。在正常情况下,函数会将当前工作路径拷贝至buf缓冲区,并返回buf作为结果。如果执行出错,则返回NULL。
注意📍
在部分IDE环境下,程序的当前工作路径可能与程序CreateSquareTable的路径不一致。在文件管理器中查找程序生成的文件SquareTable.txt时,应以示例程序实际执行输出的路径为准。
🚩第10 ~ 14行:使用fopen()函数以“文本写”模式打开文件。fopen()函数由头文件stdio.h引入,其原型为:
1 | FILE* fopen(const char* filename, const char* mode); |
其中,filename是要打开的文件名。此处的SquareTable.txt未给出从根目录开始的绝对路径,其为相对路径。fopen()函数将在当前工作目录中打开该文件。由于该文件在程序执行之前事实上不存在且打开模式被设定为“文本写”,fopen()将新建该文件。
mode为文件打开模式,它设定了文件打开的目的和操作方式,详情见表20-2。当文件打开模式未说明是文本(text)还是二进制(binary)时,C语言默认以文本形式操作文件。故w等价于wt,r等价于rt。
模式 | 用途 |
---|---|
r | 打开文本(text)文件,只读(read)不写;如文件不存在,则打开失败;等价于rt。 |
w | 打开文本文字,只写(write)不读;如文件不存在则新建;如文件存在,则截断(清空)原文件内容;等价于wt。 |
a | 以附加(append)写模式打开文本文件;如文件不存在,则打开失败;所谓附加写,是指文件打开后,向文件写入的内容将会附加在文件的原有内容之后。 |
r+ | 以读写模式打开文本文件;如文件不存在,则打开失败;所谓读写模式,是指文件打开后,既可以从文件中读取内容,也可以向文件写入内容。 |
w+ | 以写读模式打开文本文件;如文件不存在则新建,如文件存在,则截断(清空)原文件内容。 |
a+ | 以附加写并读取模式打开文本文件;如文件不存在则新建,如文件存在,从末尾追加写。 |
rb | 打开二进制(binary)文件,只读不写;如文件不存在,则打开失败。 |
wb | 打开二进制文件,只写不读;如文件不存在则新建,如文件存在,截断原内容。 |
ab | 以附加写模式打开二进制文件;如文件不存在则新建,如文件存在,从末尾追加写。 |
rb+ | 以读写模式打开二进制文件;如文件不存在,则打开失败。 |
wb+ | 以写读模式打开二进制文件;如文件不存在则新建,如文件存在,则截断(清空)原文件内容。 |
ab+ | 以附加写并读取模式打开二进制文件;如文件不存在则新建,写入时总是在末尾追加写。 |
如果fopen()函数打开文件失败,会返回空指针。上述代码的第11行将fopen()函数的返回值赋值给变量fp,然后再将赋值操作符的返回值与NULL做比较,若返回值为空,则报错并返回-1。回顾一下,表达式a=b除了把b赋值给a之外,还会返回b值做为表达式的结果。
如果fopen()函数成功打开文件,则会返回一个指向FILE结构的指针,该指针将作为后续文件读写操作的凭据。
🚩第16 ~ 20行:通过fprintf()及fputs()函数向文件中写入由字符串文本所构成的表格。下方同时列出了fprintf()与printf()的函数原型:
1 | int fprintf(FILE * _File, const char * _Format, ...); |
容易看出,fprintf()的使用方法及功能与printf()十分相似。fprintf()的第1个参数为指向FILE结构的指针,用于表明被写入的文件。在上述代码的第19行,占位符%14d代表一个输出宽度为14个字符的整数,当实际值少于14个字符时,左边补空格。fprintf()的返回值代表实际写入文件的字符数,如果写入失败,则会返回-1。
表20-3总结了C语言中常用的文本文件读写函数。
函数 | 说明 |
---|---|
fscanf | int fscanf(FILE * _File,const char * _Format, …); 用途:按指定格式从文件读取内容,使用方法类似于scanf()。 |
fprintf | int fprintf(FILE * _File, const char * _Format, …); 用途:向文件中写入格式化字符串,使用方法类似于printf()。 |
fgetc | int fgetc(FILE *_File); 用途:从文件读取一个字符,使用方法类似于getchar()。 |
fputc | int fputc(int _Ch, FILE *_File); 用途:向文件_File写入字符_Ch,使用方法类似于putchar()。 |
fgets | char * fgets(char * _Buf,int _MaxCount ,FILE * _File); 用途:从文件_File读取一个字符串至_Buf缓冲区,最多读取_MaxCount-1个字符。这里的_MaxCount事实上表明了缓冲区的大小,由于C风格的字符串必须以0结尾,所以事实上能够读取的字符个数比缓冲区尺寸少1。该函数的使用方法类似于gets()。 |
fputs | int fputs(const char * _Str, FILE * _File); 用途:将C风格的以0结尾的字符串_Str写入文件_File,使用方法类似于puts()。 |
要点🎯
对于文件文件,程序向文件写入内容时提供的是字符串,最终被写入文件的是字符按特定编码(此处是ASCII码)转换后的字节流(byte stream);当程序从文件读取内容时,实际从文件读得的是字节流,但fgets()、fgetc()内部会将字节流转换成字符串或者字符,而fscanf()则会进一步地把字符串按格式说明转换成整数、浮点数或者其它类型。这种字符与字节之间的转换是由上述读写函数自动完成的,读者在逻辑上可以认为文件就是字符流(char stream)。
注意📍
文件存储于外部存储器,变量存储于内部存储器,两者的读取速度存在数量级差异。为了避免频繁地读写外部存储器,FILE结构中引入了缓冲区机制:函数可能会提前读取成块的文件内容至内存,也可能会在积累了一定量的“写入”数据后再一次性写入文件。基于上述理由,fprintf()函数的返回并不意味着相关内容已经事实上写入了文件,真正的写入操作很可能发生在文件被关闭时。程序员可以执行fflush()函数将缓冲区内的待写入数据强行写入文件。
🚩第22 ~ 25行:当文件读/写操作完成后,应尽快关闭文件。函数fclose()如果成功关闭文件,将返回0,否则返回-1。如果函数返回值不等于0,则表示文件关闭出错,打印相关错误信息并返回。
下述代码的实际效果与前述代码完全等同:当fclose(fp)关闭文件失败返回-1时,按非零即真原则,该逻辑判断为真,意为文件关闭失败。
1 | 22 if (fclose(fp)){ //按非零即真原则判断文件是否关闭失败 |
但从软件工程的代码可读性角度来看,后者的代码容易被理解成:如果文件成功关闭,打印错误信息并返回。而前者的代码具备良好的自解释性:返回值如果不等于0,代表关闭失败。掌握了语言的语法规则只是程序设计道路上万里长征的第一步,程序设计的真实能力需要在长期的实践中摸索和总结。
在上述代码中,fp所指向的FILE结构由fopen()函数创建,fp只是指向该结构对象的指针。合理推测,fclose()函数将会释放fp所指向的FILE结构对象。
注意📍
多数C语言库函数返回0值表示操作成功,非零值表示操作失败。这有点反直觉,请读者予以关注。
在大多数运行环境里,文件的真正的读写操作事实上是由操作系统来完成的,表20-3所列的读写函数通过调用操作系统的应用编程接口(Application Programming Interface)来完成实际工作。
下述程序使用表20-3中的fgets()、fscanf()函数将SquareTable.txt中的内容读取并打印出来。
1 | //Project - ReadSquareTable |
上述程序的执行结果为(前6行):
1 | N N^2 |
🚩第6行:文件的绝对路径,其中的…意为该路径不完整,有省略。由于文件SquareTable.txt由其他程序创建,其很可能不位于程序ReadSquareTable的当前工作路径中,因此需要提供绝对路径或者恰当的相对路径。请读者按照实际情况进行修改。请注意,较新版本的Windows也允许使用/作为路径分隔符,考虑到\在C/C++中被用作转义符,为避免频繁录入\\的麻烦,这里我们使用了/作为路径分隔符。
🚩第7行:以文本只读方式打开文件。如表20-2所示,文件打开模式中的r代表读(read),t代表文本(text)。
🚩第12~14行:通过fgets()函数从文件读取一个字符串并打印出来。这个字符串预期以换行符或者文件结尾(EOF, end of file)作为结束标志。函数会在读取终止后自动在缓冲区末尾添加表示字符串结束的0值。如果函数在遇到EOF未读到字符,将返回NULL表示读取失败。
🚩第18~24行:通过fscanf()函数从文件读取整数值并打印出来。第20行可见,fscanf()在使用方式上与scanf()非常相似。在正常情况下,函数将返回读取成功的项数。本例中,该值预期为2应大于0。如果遇到文件尾导致读取失败,函数将返回EOF(-1),进而导致break的执行并结束“死”循环。EOF为一个宏,其定义如下:
1 |