本节主要讨论如何使用C语言读写文本文件。
本文引用自作者编写的下述图书; 本文允许以个人学习、教学等目的引用、讲授或转载,但需要注明原作者"海洋饼干叔
叔";本文不允许以纸质及电子出版为目的进行抄摘或改编。
1.《Python编程基础及应用》,陈波,刘慧君,高等教育出版社。免费授课视频 Python编程基础及应用
2.《Python编程基础及应用实验教程》, 陈波,熊心志,张全和,刘慧君,赵恒军,高等教育出版社Python编程基础及应用实验教程
3. 《简明C及C++语言教程》,陈波,待出版书稿。免费授课视频
在C语言里,操作一个文件的过程分为如下四步:①定义文件指针;②使用fopen()函数打开文件;③进行文件读写操作;④关闭文件。
我们通过下述示例来介绍上述过程。
//Project - CreateSquareTable
#include
#include
int main(){
char sPath[512];
if (getcwd(sPath,512)!=NULL) //获取并打印当前工作路径
printf("Current working directory: %s\n",sPath);
FILE *fp = NULL;
if ((fp=fopen("SquareTable.txt","wt"))==NULL){
printf("File open error - SquareTable.txt.\n");
return -1; //返回非零值表示程序出错
}
fprintf(fp,"%6s%14s\n","N","N^2");
fputs("--------------------\n",fp);
for (int n=1;n<=20;n++){
fprintf(fp,"%6d%14d\n",n,n*n);
}
if (fclose(fp)!=0){
printf("File close error - SquareTable.txt.\n");
return -1;
}
printf("File created & writed successfully: %s/SquareTable.txt",sPath);
return 0;
}
在作者的计算机上,上述程序的执行结果为:
Current working directory: D:\C2Cpp\C20_FileIO\build-CreateSquareTable-Desktop_Qt_5_14_1_MinGW_64_bit-Debug
File created & writed successfully: D:\C2Cpp\C20_FileIO\build-CreateSquareTable-Desktop_Qt_5_
除此之外,程序还在当前工作目录创建了一个名为SquareTable.txt的新文件,在资源管理器/文件管理器中找到这个文件并用记事本打开,可见其内容如下(前10行),这是一个平方值表。
N N^2
--------------------
1 1
2 4
3 9
4 16
5 25
6 36
7 49
8 64
...
第6 ~ 8行:使用getcwd()函数获取程序的当前工作路径(Current Working Directory)并打印至屏幕。该函数由第3行的unistd.h头文件引入,其原型如下:
char* getcwd(char* buf, int size);
参数buf应指向预分配好的缓冲区(字符数组),size则为该缓冲区的大小。在正常情况下,函数会将当前工作路径拷贝至buf缓冲区,并返回buf作为结果。如果执行出错,则返回NULL。
注意 | 在部分IDE环境下,程序的当前工作路径可能与程序CreateSquareTable的路径不一致。在文件管理器中查找程序生成的文件SquareTable.txt时,应以示例程序实际执行输出的路径为准。 |
---|
第10 ~ 14行:使用fopen()函数以“文本写”模式打开文件。fopen()函数由头文件stdio.h引入,其原型为:
FILE* fopen(const char* filename, const char* mode);
其中,filename是要打开的文件名。此处的SquareTable.txt未给出从根目录开始的绝对路径,其为相对路径。fopen()函数将在当前工作目录中打开该文件。由于该文件在程序执行之前事实上不存在且打开模式被设定为“文本写”,fopen()将新建该文件。
mode为文件打开模式,它设定了文件打开的目的和操作方式,详情见表20-2。当文件打开模式未说明是文本(text)还是二进制(binary)时,C语言默认以文本形式操作文件。故w等价于wt,r等价于rt。
表20-2 C语言的常用文件打开模式
模式 | 用途 |
---|---|
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()的函数原型:
int fprintf(FILE * _File, const char * _Format, ...);
int printf(const char * _Format, ...);
容易看出,fprintf()的使用方法及功能与printf()十分相似。fprintf()的第1个参数为指向FILE结构的指针,用于表明被写入的文件。在上述代码的第19行,占位符%14d代表一个输出宽度为14个字符的整数,当实际值少于14个字符时,左边补空格。fprintf()的返回值代表实际写入文件的字符数,如果写入失败,则会返回-1。
表20-3总结了C语言中常用的文本文件读写函数。
表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时,按非零即真原则,该逻辑判断为真,意为文件关闭失败。
22 if (fclose(fp)){ //按非零即真原则判断文件是否关闭失败
23 printf("File close error - SquareTable.txt.\n");
24 return -1;
25 }
但从软件工程的代码可读性角度来看,后者的代码容易被理解成:如果文件成功关闭,打印错误信息并返回。而前者的代码具备良好的自解释性:返回值如果不等于0,代表关闭失败。掌握了语言的语法规则只是程序设计道路上万里长征的第一步,程序设计的真实能力需要在长期的实践中摸索和总结。
在上述代码中,fp所指向的FILE结构由fopen()函数创建,fp只是指向该结构对象的指针。合理推测,fclose()函数将会释放fp所指向的FILE结构对象。
注意 | 多数C语言库函数返回0值表示操作成功,非零值表示操作失败。这有点反直觉,请读者予以关注。在大多数运行环境里,文件的真正的读写操作事实上是由操作系统来完成的,表20-3所列的读写函数通过调用操作系统的应用编程接口(Application Programming Interface)来完成实际工作。 |
---|
下述程序使用表20-3中的fgets()、fscanf()函数将SquareTable.txt中的内容读取并打印出来。
//Project - ReadSquareTable
#include
int main(){
FILE *fp = NULL;
char sFile[] = "D:/C2Cpp/C20_FileIO/...Debug/SquareTable.txt";
if ((fp=fopen(sFile,"rt"))==NULL){
printf("File open error - SquareTable.txt.\n");
return -1; //返回非零值表示程序出错
}
char sBuffer[512];
if (fgets(sBuffer,512,fp))
printf("%s",sBuffer);
if (fgets(sBuffer,512,fp))
printf("%s",sBuffer);
int n=0, n2=0;
while (1){
if (fscanf(fp,"%d %d",&n,&n2)>0)
printf("%6d%14d\n",n,n2);
else
break;
}
if (fclose(fp)!=0){
printf("File close error - SquareTable.txt.\n");
return -1;
}
return 0;
}
上述程序的执行结果为(前6行):
N N^2
--------------------
1 1
2 4
3 9
4 16
...
第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为一个宏,其定义如下:
#define EOF (-1)
为了帮助更多的年轻朋友们学好编程,作者在B站上开了两门免费的网课,一门零基础讲Python,一门零基础C和C++一起学,拿走不谢!
简洁的C及C++
Python编程基础及应用
如果你觉得纸质书看起来更顺手,目前Python有两本,C和C++在出版过程中。
Python编程基础及应用
Python编程基础及应用实验教程