本节主要讨论如何使用C语言读写文本文件。

  在C语言里,操作一个文件的过程分为如下四步:①定义文件指针;②使用fopen()函数打开文件;③进行文件读写操作;④关闭文件。

版权声明

本文可以在互联网上自由转载,但必须:注明出处(作者:海洋饼干叔叔)并包含指向本页面的链接。

本文不可以以纸质出版为目的进行改编、摘抄。

  我们通过下述示例来介绍上述过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//Project - CreateSquareTable
#include <stdio.h>
#include <unistd.h>

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;
}

在作者的计算机上,上述程序的执行结果为:

1
2
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_14_1_MinGW_64_bit-Debug/SquareTable.txt

  除此之外,程序还在当前工作目录创建了一个名为SquareTable.txt的新文件,在资源管理器/文件管理器中找到这个文件并用记事本打开,可见其内容如下(前10行),这是一个平方值表。

1
2
3
4
5
6
7
8
9
10
11
     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头文件引入,其原型如下:

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。

表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()的函数原型:

1
2
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时,按非零即真原则,该逻辑判断为真,意为文件关闭失败。

1
2
3
4
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中的内容读取并打印出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//Project - ReadSquareTable
#include <stdio.h>

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行):

1
2
3
4
5
6
7
     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为一个宏,其定义如下:

1
#define EOF (-1)