第 13章 文 件
13.1 C文件概述
13.2 文件类型指针
13.3 文件的打开与关闭
13.4 文件的读写
13.5 文件的定位
13.6 出错的检测
13.7 文件输入输出小结习题
13.1 C文件概述
文件 (file)是程序设计中一个重要的概念。所谓,文件,
一般指存储在外部介质上数据的集合。一批数据是以文件的形式存放在外部介质 (如磁盘 )上的。操作系统是以文件为单位对数据进行管理的,也就是说,如果想找存在外部介质上的数据,必须先按文件名找到所指定的文件,然后再从该文件中读取数据。要向外部介质上存储数据也必须先建立一个文件 (以文件名标识 ),才能向它输出数据。
以前各章中所用到的输入和输出,都是以终端为对象的,即从终端键盘输入数据,运行结果输出到终端上。
从操作系统的角度看,每一个与主机相联的输入输出设备都看作是一个文件。例如,终端 键盘是输入文件,
显示屏和打印机是输出文件。
在程序运行时,常常需要将一些数据 (运行的最终结果或中间数据 )输出到磁盘上存放起来,以后需要时再从磁盘中输入到计算机内存。这就要用到磁盘文件。
C语言把文件看作是一个字符 (字节 )的序列,即由一个一个字符 (字节 )的数据顺序组成。根据数据的组织形式,可分为 ASCII文件和二进制文件。
ASCII文件又称文本 (text)文件,它的每一个字节放一个 ASCII代码,代表一个字符。二进制文件是把内存中的数据按其在内存中的存储形式原样输出到磁盘上存放。如果有一个整数 10000,在内存中占 2
个字节,如果按 ASCII码形式输出,则占 5个字节,
而按二进制形式输出,在磁盘上只占 2个字节,见图 13.1。用 ASCII码形式输出与字符一一对应,一个字节代表一个字符,一个字节代表一个字符,
因而便于对字符进行逐个处理,也便于输出字符。
但一般占存储空间较多,而且要花费转换时间 (二进制形式与 ASCII码间的转换 )。用二进制形式输出数值,可以节省外存空间和转换时间,但一个字节并不对应一个字符,不能直接输出字符形式。
一般中间结果数据需要暂时保存在外存上以后又需要输入到内存的,常用二进制文件保存。
图 13.1
由前所述,一个 C文件是一个字节流或二进制流。
它把数据看作是一连串的字符 (字节 ),而不考虑记录的界限。换句话说,C语言中文件并不是由记录 (record)组成的 (这是和 PASCAL或其他高级语言不同的 )。在 C语言中对文件的存取是以字符 (字节 )
为单位的。输入输出的数据流的开始和结束仅受程序控制而不受物理符号 (如回车换行符 )控制。
也就是说,在输出时不会自动增加回车换行符以作为记录结束的标志,输入时不以回车换行符作为记录的间隔 (事实上 C文件并不由记录构成 )。我们把这种文件称为流式文件。 C语言允许对文件存取一个字符,这就增加了处理的灵活性。
在过去使用的 C版本 (如 UNIX系统下使用的 C)有两种对文件的处理方法:一种叫“缓冲文件系统”,
一种叫“非缓冲文件系统”。所谓缓冲文件系统是指系统自动地在内存区为每一个正在使用的文件名开辟一个缓冲区。从内存向磁盘输出数据必须先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘去。如果从磁盘向内存读入数据,则一次从磁盘文件将一批数据输入到内存缓冲区 (充满缓冲区 ),然后再图 13.2
从缓冲区逐个地将数据送到程序数据区 (给程序变量 )。见图 13.2。缓冲区的大小由各个具体的 C版本确定,一般为 512字节。
所谓“非缓冲文件系统”是指系统不自动开辟确定大小的缓冲区,而由程序为每个文件设定缓冲区。
在 UNIX系统下,用缓冲文件系统来处理文本文件,
用非缓冲文件系统处理二进制文件。用缓冲文件系统进行的输入输出又称为高级 (或高层 )磁盘输入输出 (高层 I/O),用非缓冲文件系统进行的输入输出又称为低级 (低层 )输入输出系统。 ANSI C标准决定不采用非缓冲文件系统,而只采用缓冲文件系统。即既用缓冲文件系统处理文本文件,也用它来处理二进制文件。也就是将缓冲文件系统扩充为可以处理二进制文件。
在 C语言中,没有输入输出语句,对文件的读写都是用库函数来实现的。 ANSI规定了标准输入输出函数,用它们对文件进行读写。
本章只介绍 ANSI C规定的文件系统以及对它的读写。
13.2 文件类型指针
缓冲文件系统中,关键的概念是“文件指针”。
每个被使用的文件都在内存中开辟一个区,用来存放文件的有关信息 (如文件的名字、文件状态及文件当前位置等 )。这些信息是保存在一个结构体变量中的。该结构体类型是由系统定义的,取名为 FILE。 Turbo C在 stdio.h文件中有以下的文件类型声明:
typedef struct
{ shortlevel; /*缓冲区“满”或“空”的程度 */
unsignedflags; /*文件状态标志 */
charfd; /*文件描述符 */
unsignedcharhold; /*如无缓冲区不读取字符 */
shortbsize; /*缓冲区的大小 */
unsignedchar*baffer;/*数据缓冲区的位置 */
unsignedar*curp;/*指针,当前的指向 */
unsignedistemp;/*临时文件,指示器 */
shorttoken;/*用于有效性检查 */
} FILE;
有了结构体 FILE类型之后,可以用它来定义若干个 FILE类型的变量,以便存放若干个文件的信息。
例如,可以定义以下 FILE类型的数组。 FILE
f[5];定义了一个结构体数组 f,它有 5个元素,可以用来存放 5个文件的信息。
可以定义文件型指针变量。如,
FILE fp;
fp是一个指向 FILE类型结构体的指针变量。可以使 fp指向某一个文件的结构体变量,从而通过该结构体变量中的文件信息能够访问该文件。也就是说,通过文件指针变量能够找到与它相关的文件。如果有 n个文件,一般应设 n个指针变量 (指向
FILE类型结构体的指针变量 ),使它们分别指向
n个文件 (确切地说指向存放该文件信息的结构体变量 ),以实现对文件的访问。
13.3 文件的打开与关闭
和其他高级语言一样,对文件读写之前应该“打开”该文件,在使用结束之后应关闭该文件。
13.3.1 文件的打开 (fopen函数 )
ANSI C规定了标准输入输出函数库,用 fopen( )
函数来实现打开文件。
fopen函数的调用方式通常为
FILE fp;
fp=fopen(文件名,使用文件方式 );
例如:
fp=fopen("a1","r");
它表示要打开名字为 a1的文件,使用文件方式为
“读入” (r代表 read,即读入 ),fopen函数带回指向 a1文件的指针并赋给 fp,这样 fp就和文件 a1相联系了,或者说,fp指向 a1文件。可以看出,在打开一个文件时,通知给编译系统以下 3个信息:
①需要打开的文件名,也就是准备访问的文件的名字。
② 使用文件的方式 (“读”还是“写”等 )。
③让哪一个指针变量指向被打开的文件。
说明:
(1) 用,r”方式打开的文件只能用于向计算机输入而不能用作向该文件输出数据,而且该文件应该已经存在,不能用,r”方式打开一个并不存在的文件 (即输入文件 ),否则出错。
(2) 用,w”方式打开的文件只能用于向该文件写数据 (即输出文件 ),而不能用来向计算机输入。如果原来不存在该文件,则在打开时新建立一个以指定的名字命名的文件。如果原来已存在一个以该文件名命名的文件,则在打开时将该文件删去,
然后重新建立一个新文件。
(3) 如果希望向文件末尾添加新的数据 (不希望删除原有数据 ),则应该用,a”方式打开。但此时该文件必须已存在,否则将得到出错信息。打开时,
位置指针移到文件末尾。
(4) 用,r+”、,w+”、,a+”方式打开的文件既可以用来输入数据,也可以用来输出数据。用,r+”
方式时该文件应该已经存在,以便能向计算机输入数据。用,w+”方式则新建立一个文件,先向此文件写数据,然后可以读此文件中的数据。用
,a+”方式打开的文件,原来的文件不被删去,位置指针移到文件末尾,可以添加,也可以读。
(5) 如果不能实现“打开”的任务,fopen函数将会带回一个出错信息。出错的原因可能是用,r”
方式打开一个并不存在的文件 ;磁盘出故障 ;磁盘已满无法建立新文件等。此时 fopen函数将带回一个空指针值 NULL(NULL在 stdio,h文件中已被定义为
0)。
常用下面的方法打开一个文件:
if ((fp=fopen("file1","r"))==NULL)
{ printf("cannot open this file\n");
exit(0);
}
即先检查打开的操作有否出错,如果有错就在终端上输出,cannot open this file”。 exit函数的作用是关闭所有文件,终止正在调用的过程。待用户检查出错误,修改后再运行。
(6) 用以上方式可以打开文本文件或二进制文件,
这是 ANSI C的规定,用同一种缓冲文件系统来处理文本文件和二进制文件。但目前使用的有些 C
编译系统可能不完全提供所有这些功能 (例如有的只能用,r”、,w”、,a”方式 ),有的 C版本不用
,r+”、,w+”、,a+”,而用,rw”、,wr”、
,ar”等,请读者注意所用系统的规定。
(7) 在向计算机输入文本文件时,将回车换行符转换为一个换行符,在输出时把换行符转换成为回车和换行两个字符。在用二进制文件时,不进行这种转换,在内存中的数据形式与输出到外部文件中的数据形式完全一致,一一对应。
(8) 在程序开始运行时,系统自动打开 3个标准文件:标准输入、标准输出、标准出错输出。通常这 3个文件都与终端相联系。因此以前我们所用到的从终端输入或输出都不需要打开终端文件。系统自动定义了 3个文件指针 stdin,stdout和 stderr,
分别指向终端输入、终端输出和标准出错输?也从终端输出 )。如果程序中指定要从 stdin所指的文件输入数据,就是指从终端键盘输入数据。
13.3.2 文件的关闭 (fclose函数 )
在使用完一个文件后应该关闭它,以防止它再被误用。“关闭”就是使文件指针变量不指向该文件,
也就是文件指针变量与文件“脱钩”,此后不能再通过该指针对原来与其相联系的文件进行读写操作。除非再次打开,使该指针变量重新指向该文件。
用 fclose函数关闭文件。 fclose函数调用的一般形式为
fclose(文件指针 );
例如:
fclose(fp);
前面我们曾把打开文件 (用 fopen函数 )时所带回的指针赋给了 fp,今通过 fp把该文件关闭。即 fp不再指向该文件。
应该养成在程序终止之前关闭所有文件的习惯,
如果不关闭文件将会丢失数据。因为,如前所述,
在向文件写数据时,是先将数据输到缓冲区,待缓冲区充满后才正式输出给文件。如果当数据未充满缓冲区而程序结束运行,就会将缓冲区中的数据丢失。用 fclose函数关闭文件,可以避免这个问题,它先把缓冲区中的数据输出到磁盘文件,
然后才释放文件指针变量。
fclose函数也带回一个值,当顺利地执行了关闭操作,则返回值为 0;否则返回 EOF(-1)。可以用
ferror函数来测试 (见 13.6,1节 )。
13.4 文 件 的 读 写
文件打开之后,就可以对它进行读写了。常用的读写函数如下所述。
13.4.1 fputc函数和 fgetc函数 (putc函数和 getc函数 )
1,fputc函数
把一个字符写到磁盘文件上去。其一般调用形式为
fputc(ch,fp);
其中 ch是要输出的字符,它可以是一个字符常量,
也可以是一个字符变量。 fp是文件指针变量。
fputc(ch,fp)函数的作用是将字符 (ch的值 )输出到
fp所指向的文件中去。 fputc函数也带回一个值:
如果输出成功则返回值就是输出的字符 ;如果输出失败,则返回一个 EOF(-1)。 EOF是在 stdio,h文件中定义的符号常量,值为 -1。在第 4章介绍过
putchar函数,其实 putchar是从 fputc函数派生出来的。 putchar(c)是在 stdio.h文件中用预处理命令
#define定义的宏,#define putchar(c) fputc(c,
stdout)前面已叙述,stdout是系统定义的文件指针变量,它与终端输出相连。 fputc(c瑂 tdout)的作用是将 c的值输出到终端。用宏 putchar(c)比写
fputc(c,stdout)简单一些。从用户的角度,可以把 putchar(c)看作函数而不必严格地称它为宏。
2,fgetc函数
从指定的文件读入一个字符,该文件必须是以读或读写方式打开的。 fgetc函数的调用形式为
ch=fgetc(fp);
fp为文件型指针变量,ch为字符变量。 fgetc函数带回一个字符,赋给 ch。如果在执行 fgetc函数读字符时遇到文件结束符,函数返回一个文件结束标志 EOF(-1)。如果想从一个磁盘文件顺序读入字符并在屏幕上显示出来,可以:
ch=fgetc(fp);
while(ch! =EOF)
{ putchar(ch);
ch=fgetc(fp);
}
注意,EOF不是可输出字符,因此不能在屏幕上显示。由于字符的 ASCII码不可能出现 -1,因此 EOF
定义为 -1是合适的。当读入的字符值等于 -1(即 EOF)
时,表示读入的已不是正常的字符而是文件结束符。
但以上只适用于读文本文件的情况。现在 ANSI C
已允许用缓冲文件系统处理二进制文件,而读入某一个字节中的二进制数据的值有可能是 -1,而这又恰好是 EOF的值。这就出现了需要读入有用数据而却被处理为“文件结束”的情况。为了解决这个问题,ANSI C提供一个 feof函数来判断文件是否真的结束。 feof(fp)用来测试 fp所指向的文件当前状态是否“文件结束”。如果是文件结束,函数 feof(fp)的值为 1(真 ),否则为 0(假 )。
如果想顺序读入一个二进制文件中的数据,可以用
while(! feof(fp))
{ c=fgetc(fp);
…
}
当未遇文件结束,feof(fp)的值为 0,! feof(fp)为 1,
读入一个字节的数据赋给整型变量 c,并接着对其进行所需的处理。直到遇文件结束,feof(fp)值为
1,! feof(fp)值为 0,不再执行 while循环。
这种方法也适用于文本文件。
3,fputc和 fgetc函数使用举例
在掌握了以上几种函数以后,可以编制一些简单的使用文件的程序。
例 13.1从键盘输入一些字符,逐个把它们送到磁盘上去,直到输入一个,#”为止。
#include <stdio,h>
main( )
{ FILE fp;
char ch,filename[10];
scanf("%s",filename);
if((fp=fopen(filename,"w"))==NULL)
{ printf("cannot open file\n");
exit(0);}
ch=getchar( );/*此语句用来接收在执行
scanf语句时最后输入的回车符 */
ch=getchar( );/*接收输入的第一个字符 */
while(ch! ='#')
{
fputc(ch,fp);putchar(ch);
ch=getchar();
}
fclose(fp);
}
运行情况如下:
file1,c (输入磁盘文件名 )
computer and c# (输入一个字符串 )
computer and c (输出一个字符串 )
文件名由键盘输入,赋给字符数组 filename,
fopen函数中的第一个参数“文件名”可以直接写成字符串常量形式 (如,file1,c”),也可以用字符数组名,在字符数组中存放文件名 (如本例所用的方法 )。本例运行时,从键盘输入磁盘文件名
,file1,c”,然后输入要写入该磁盘文件的字符
,computer and c”,,#”是表示输入结束,程序将,computer and c”写到以,file1,c”命名的磁盘文件中,同时在屏幕上显示这些字符,以便核对。
可以用 DOS命令将 file1,c文件中的内容打印出来:
C> type file1,c
computer and c
证明了在 file1,c文件中已存入了,computer and
c”的信息。
例 13.2将一个磁盘文件中的信息复制到另一个磁盘文件中。
#include <stdio,h>
main( )
{ FILE in out;
char ch,infile[10],outfile[10];
printf("Enter the infile name,\n");
scanf("%s",infile);
printf("Enter the outfile name,\n");
scanf("%s",outfile);
if((in=fopen(infile,"r"))==NULL)
{ printf("cannot open infile\n");
exit(0);
}
if((out=fopen(outfile,"w"))==NULL)
{ printf("cannot open outfile\n");
exit(0);
}
while(! feof(in))fputc(fgetc(in),out);
fclose(in);
fclose(out);
}
运行情况如下:
Enter the infile name:
file1,c (输入原有磁盘文件名 )
Enter the outfile name:
file2,c (输入新复制的磁盘文件名 )
程序运行结果是将 file1,c文件中的内容复制到
file2,c中去。可以用下面 DOS命令验证
c> type file1,c
computer and c(file1,c中的信息 )
c> type file2,c
computer and c (file2,c中的信息 )
以上程序是按文本文件方式处理的。也可以用此程序来复制一个二进制文件,只需将两个 fopen函数中的,r”和,w”分别改为,rb”和,wb”即可。
也可以在输入命令行时把两个文件名一起输入。
这时要用到 main函数的参数。程序可改为
#include <stdio,h>
main(int argc,char*argv[ ])
{ FILE in,*out;
char ch;
if (argc! =3)
{ printf("You forgot to enter a filename\n");
exit(0);
}
if((in=fopen(argv[1],"r"))==NULL)
{ printf("cannot open infile\n");
exit(0);
}
if((out=fopen(argv[2],"w"))==NULL)
{ printf("cannot open outfile\n");
exit(0);
}
while(!feof(in)) fputc(fgetc(in),out);
fclose(in);
fclose(out);
}
假若本程序的源文件名为 a.c,经编译连接后得到的可执行文件名为 a.exe,则在 DOS命令工作方式下,可以输入以下的命令行:
C> a file1,c file2,c
即在键入可执行文件名后,再输入两个参数
file1,c和 file2,c,分别输入到 argv[1]和 argv[2]
中,argv[0]的内容为 a,argc的值等于 3(因为此命令行共有 3个参数 )。如果输入的参数少于 3个,则程序会输出:“你忘了输入一个文件名”。程序执行结果是将 file1,c中的信息复制到 file2,c中。
可以用以下命令验证:
C> type file1,c
computer and c
(这是 file1,c文件中的信息 )
C> type file2,c
computer and c
(这是 file2,c文件中的信息。可见 file1,c已复制到 file2,c中了 )。
最后说明一点,为了书写方便,系统把 fputc和
fgetc定义为宏名 putc和 getc:
#define putc(ch,fp) fputc(ch,fp)
#define getc(fp) fgetc(fp)
这是在 stdio,h中定义的。因此,用 putc和 fputc及用 getc和 fgetc是一样的。一般可以把它们作为相同的函数来对待。
13.4.2 fread函数和 fwrite函数
用 getc和 putc函数可以用来读写文件中的一个字符。
但是常常要求一次读入一组数据 (例如,一个实数或一个结构体变量的值 ),ANSI C标准提出设置两个函数 (fread和 fwrite),用来读写一个数据块。它们的一般调用形式为
fread(buffer,siz e,count,fp);
fwrite(buffer,siz e,count,fp);
其中:
buffer:是一个指针。对 fread来说,它是读入数据的存放地址。对 fwrite来说,是要输出数据的地址 (以上指的是起始地址 )。
size:要读写的字节数。
count:要进行读写多少个 siz e字节的数据项。
fp:文件型指针。
如果文件以二进制形式打开,用 fread和
fwrite函数就可以读写任何类型的信息,如:
fread(f,4,2,fp);
其中 f是一个实型数组名。一个实型变量占 4个字节。
这个函数从 fp所指向的文件读入 2次 (每次 4个字节 )
数据,存储到数组 f中。
如果有一个如下的结构体类型:
struct student-type
{ char name[10];
int num;
int age;
char addr[30];
} stud[40];
结构体数组 stud有 40个元素,每一个元素用来存放一个学生的数据 (包括姓名、学号、年龄、地址 )。
假设学生的数据已存放在磁盘文件中,可以用下面的 for语句和 fread函数读入 40个学生的数据:
for(i=0;i< 40;i++)
fread(&stud[i],sizeof(struct student-type),1,fp);
同样,以下 for语句和 fwrite函数可以将内存中的学生数据输出到磁盘文件中去:
for(i=0;i< 40,i++)
fwrite(&stud[i],sizeof(struct student-type),1,fp);
如果 fread或 fwrite调用成功,则函数返回值为
count的值,即输入或输出数据项的完整个数。
下面写出一个完整的程序。
例 13.3从键盘输入 4个学生的有关数据,然后把它们转存到磁盘文件上去。
#include <stdio,h>
#define SIZE 4
struct student-type
{ char name[10];
int num;
int age;
char addr[15];
} stud[SIZE];
void save( )
{ FILE fp;
int i;
if((fp=fopen("stu-list","wb"))==NULL)
{ printf("cannot open file\n");
return;
}
for(i=0;i< SIZE;i++)
if(fwrite(&stud[i],sizeof(struct student-
type),1,fp)!=1)
printf("file write error\n");
fclose (fp);
}
main()
{ int i;
for(i=0;i< SIZE;i++)
scanf("%s%d%d%s",stud[i].name,
&stud[i].num,&stud[i].age,stud[i].addr);
save( );
}
在 main函数中,从终端键盘输入 4个学生的数据,
然后调用 save函数,将这些数据输出到以
,stu_list”命名的磁盘文件中。 fwrite函数的作用是将一个长度为 29字节的数据块送到 stu_list文件中 (一个 student_type类型结构体变量的长度为它的成员长度之和,即 10+2+2+15=29)。运行情况如下:
输入 4个学生的姓名、学号、年龄和地址:
Zhang 1001 19 room-101
Fun 1002 20 room-102
Tan 1003 21 room-103
Ling 1004 21 room-104
程序运行时,屏幕上并无输出任何信息,只是将从键盘输入的数据送到磁盘文件上。为了验证在磁盘文件,stu_list”中是否已存在此数据可以用以下程序从,stu_list”文件中读入数据,然后在屏幕上输出。
#include <stdio,h>
#define SIZE 4
struct student-type
{ char name[10];
int num;
int age;
char addr[15];
} stud[SIZE];
main( )
{ int i;
FILE fp;
fp=fopen("stu-list","rb");
for(i=0;i< SIZE;i++)
{ fread(&stud[i],sizeof(struct student-type),1,
fp);
printf("%-10s %4d %4d %-15s\n",
stud[i].name,&stud[i].num,&stud[i],age,
stud[i].addr);
fclose (fp);
}
程序运行时不需从键盘输入任何数据。屏幕上显示出以下信息:
Zhang100119room-101
Fun 1002 20 room-102
Tan 1003 21 room-103
Ling 1004 21 room-104
请注意输入输出数据的状况。从键盘输入 4个学生的数据是 ASCII码,也就是文本文件。在送到计算机内存时,回车和换行符转换成一个换行符。再从内存以,wb”方式
(二进制写 )输出到,stu_list”文件,此时不发生字符转换,按内存中存储形式原样输出到磁盘文件上。
在上面验证程序中,又用 fread函数从,stu_list”文件向内存读入数据,注意此时用的是,rb”方式,
即二进制方式,数据按原样输入,也不发生字符转换。也就是这时候内存中的数据恢复到第一个程序向
,stu-list”输出以前的情况。最后在验证程序中,
用 printf函数输出到屏幕,printf是格式输出函数,
输出 ASCII码,在屏幕上显示字符。换行符又转换为回车加换行符。
如果企图从,stu_list”文件中以,r”方式读入数据就会出错。
fread和 fwrite函数一般用于二进制文件的输入输出。因为它们是按数据块的长度来处理输入输出的,在字符发生转换的情况下很可能出现与原设想的情况不同。
例如,如果写 fread(&stud[i],sizeof(struct
student-type),1,stdin);
企图从终端键盘输入数据,这在语法上并不存在错误,编译能通过。如果用以下形式输入数据:
Zhang 1001 10 room-101
…
由于 fread函数要求一次输入 29个字节 (而不问这些字节的内容 ),因此输入数据中的空格也作为输入数据而不作为数据间的分隔符了。连空格也存储到 stud[i]中了,显然是不对的。
这个题目要求的是从键盘输入数据,如果已有的数据已以二进制形式存储在一个磁盘文件,stu-
dat”中,要求从其中读入数据并输出到,stu-list”
文件中,可以编写一个 load函数,从磁盘文件中读二进制数据。
void load( )
{ FILE fp;
int i;
if((fp=fopen("stu-dat","rb"))==NULL)
{ printf("cannot open infile\n");
return;}
for(i=0;i< SIZE;i++)
if(fread(&stud[i],sizeof(struct student-type),
1,fp)!=1)
{ if(feof(fp)){fclose (fp); return;}
printf("file read error\n");
}
fclose (fp);
}
将 load函数加到本题原来的程序文件中,并将
main函数改为 main( )
{
load( );
save( );
}
13.4.3 fprintf函数和 fscanf函数
fprintf函数,fscanf函数与 printf函数,scanf函数作用相仿,都是格式化读写函数。只有一点不同:
fprintf和 fscanf函数的读写对象不是终端而是磁盘文件。它们的一般调用方式为 fprintf(文件指针,
格式字符串,输出表列 );fscanf (文件指针,格式字符串,输入表列 );例如,
fprintf(fp,"%d,%6,2f",i,t);
它的作用是将整型变量 i和实型变量 t的值按 %d和
%6,2f的格式输出到 fp指向的文件上。如果 i=3,
t=4,5,则输出到磁盘文件上的是以下的字符串:
3,4,50
同样,用以下 fscanf函数可以从磁盘文件上读入
ASCII字符,fscanf(fp,"%d,%f",&i,&t);
磁盘文件上如果有以下字符:
3,4,5
则将磁盘文件中的数据 3送给变量 i,4,5送给变量 t。
用 fprintf和 fscanf函数对磁盘文件读写,使用方便,
容易理解,但由于在输入时要将 ASCII码转换为二进制形式,在输出时又要将二进制形式转换成字符,花费时间比较多。因此,在内存与磁盘频繁交换数据的情况下,最好不用 fprintf和
fscanf函数,而用 fread和 fwrite函数。
13.4.4 其他读写函数
1,putw和 getw函数
大多数 C编译系统都提供另外两个函数,putw和
getw,用来对磁盘文件读写一个字 (整数 )。例如:
putw(10,fp);
它的作用是将整数 10输出到 fp指向的文件。而
i=getw(fp);的作用是从磁盘文件读一个整数到内存,赋给整型变量 i。
如果所用的 C编译的库函数中不包括 putw和 getw
函数,可以自己定义该两函数。 putw函数如下:
putw(int i,FILE *fp)
{ char s;
s=(char*)&i;
putc(s[0],fp);putc(s[1],fp);
return(i);
}
当调用 putw函数时,如果用,putw(10,fp);”语句,形参 i得到实参传来的值 10,在 putw函数中将 i
的地址赋予指针变量 s,而 s是指向字符变量的指针变量,因此 s指向 i的第 1个字节,s+1指向 i的第 2
个字节。由于 *(s+0)就是 s[0],*(s+1)就是 s[1],因此,s[0],s[1]分别对应 i的第 1字节和第 2个字节。
由于图 13.3
*(s+0)就是 s[0],*(s+1)就是 s[1],因此,s[0],s[1]
分别对应 i的第 1字节和第 2字节。顺序输出 s[0]、
s[1]就相当于输出了 i的两个字节中的内容。见图
13.3。
getw函数如下:
getw(FILE *fp)
{ char s;
int i;
s=(char *)&i; /*使 s指向 i的起始地址 */
s[0]=getc(fp);
s[1]=getc(fp);
return(i);
}
putw和 getw并不是 ANSI C标准定义的函数。但许多 C编译都提供这两个函数,但有的 C编译可能不以 putw和 getw命名此两函数,而用其他函数名,
请用时注意。
2,读写其他类型数据
如果用 ANSI C提供的 fread和 fwrite函数,读写任何类型数据都是十分方便的。如果所用的系统不提供这两个函数,用户只好自己定义所需函数。
例如,可以定义一个向磁盘文件写一个实数 (用二进制方式 )的函数 putfloat:
putfloat(float num,FILE *fp)
{ char s;
int count;
s=(char *)#
for(count=0;count< 4;count++)
putc(s[count],fp);
}
同样可以编写出读写任何类型数据的函数。
3,fgets函数和 fputs函数
fgets的作用是从指定文件读入一个字符串。如,
fgets(str,n,fp);
n为要求得到的字符,但只从 fp指向的文件输入 n-
1个字符,然后在最后加一个‘ \0’字符,因此得到的字符串共有 n个字符。把它们放到字符数组 str
中。如果在读完 n-1个字符之前遇到换行符或 EOF,
读入即结束。 fgets函数返回值为 str的首地址。
fputs函数的作用是向指定的文件输出一个字符串。
如,fputs("China",fp);把字符串,China”输出到
fp指向的文件。 fputs函数中第一个参数可以是字符串常量、字符数组名或字符型指针。字符串末尾的‘ \0’ 不输出。若输出成功,函数值为 0;失败时,为 EOF。
这两个函数类似以前介绍过的 gets和 puts函数,只是 fgets和 fputs函数以指定的文件作为读写对象。
13.5 文 件 的 定 位
文件中有一个位置指针,指向当前读写的位置。
如果顺序读写一个文件,每次读写一个字符,则读写完一个字符后,该位置指针自动移动指向下一个字符位置。如果想改变这样的规律,强制使位置指针指向其他指定的位置,可以用有关函数。
13.5.1 rewind函数
rewind函数的作用是使位置指针重新返回文件的开头。此函数没有返回值。
例 13.4有一个磁盘文件,第一次将它的内容显示在屏幕上,第二次把它复制到另一文件上。
#include<stdio.h>
main()
{ FILE fp1,*fp2;
fp1=fopen("file1,c","r");
fp2=fopen("file2,c","w");
while(! feof(fp1)) putchar(getc(fp1));
rewind(fp1);
while(! feof(fp1)) putc(getc(fp1),fp2);
fclose(fp1);fclose(fp2);
}
在第一次将文件的内容显示在屏幕以后,文件
file1,c的位置指针已指到文件末尾,feof的值为非零 (真 )。执行 rewind函数,使文件的位置指针重新定位于文件开头,并使 feof函数的值恢复为
0(假 )。
13.5.2 fseek函数和随机读写
对流式文件可以进行顺序读写,也可以进行随机读写。关键在于控制文件的位置指针,如果位置指针是按字节位置顺序移动的,就是顺序读写。
如果能将位置指针按需要移动到任意位置,就可以实现随机读写。所谓随机读写,是指读写完上一个字符 (字节 )后,并不一定要读写其后续的址?
字节 ),而可以读写文件中任意所需的字符 (字节 )。
用 fseek函数可以实现改变文件的位置指针。
fseek函数的调用形式为 fseek (文件类型指针,位移量,起始点 )
,起始点”用 0,1或 2代替,0代表“文件开始”,
1为“当前位置”,2为“文件末尾”。 ANSI C标准指定的名字如表 13.2所示。
,位移量”指以“起始点”为基点,向前移动的字节数。 ANSI C和大多数 C版本要求位移量是
long型数据。这样当文件的长度大于 64K时不致出问题。 ANSI C标准规定在数字的末尾加一个字母 L,就表示是 long型。
下面是 fseek函数调用的几个例子:
fseek(fp,100L,0);将位置指针移到离文件头 100
个字节处
fseek(fp,50L,1); 将位置指针移到离当前位置
50个字节处
fseek(fp,-10L,2); 将位置指针从文件末尾处向后退 10个字节
利用 fseek函数就可以实现随机读写了。
例 13.5在磁盘文件上存有 10个学生的数据。要求将第 1,3,5,7,9个学生数据输入计算机,并在屏幕上显示出来。
程序如下:
#include<stdio.h>
struct student-type
{
char name[10];
int num;
int age;
char sex;
} stud[10];
main()
{
int i;
FILE *fp;
if((fp=fopen("stud -dat","rb"))==NULL)
{printf("can not open file\n");
exit(0);}
for(i=0;i< 10;i+=2)
{
fseek(fp,i*sizeof(struct student-type),0);
fread(&stud[i],sizeof(struct student-type),1,
fp);
printf("%s %d %d %c\n",stud[i].name,
stud[i].num,stud[i].age,stud[i].sex);
}
fclose(fp);
}
13.5.3 ftell函数
ftell函数的作用是得到流式文件中的当前位置,用相对于文件开头的位移量来表示。由于文件中的位置指针经常移动,人们往往不容易知道其当前位置。用 ftell函数可以得到当前位置。如果 ftell函数返回值为 -1L,表示出错。例如:
i=ftell(fp);
if(i==-1L)printf("error\n");
变量 i存放当前位置,如调用函数出错 (如不存在此文件 ),则输出,error”。
13.6 出 错 的 检 测
C标准提供一些函数用来检查输入输出函数调用中的错误。
13.6.1 ferror函数
在调用各种输入输出函数 (如 putc,getc,fread、
fwrite等 )时,如果出现错误,除了函数返回值有所反映外,还可以用 ferror函数检查。它的一般调用形式为 ferror(fp);
如果 ferror返回值为 0(假 ),表示未出错。如果返回一个非零值,表示出错。应该注意,对同一个文件每一次调用输入输出函数,均产生一个新的
ferror函数值,因此,应当在调用一个输入输出函数后立即检查 ferror函数的值,否则信息会丢失。
在执行 fopen函数时,ferror?。
13.6.2 clearerr函数
它的作用是使文件错误标志和文件结束标志置为 0。
假设在调用一个输入输出函数时出现错误,ferror
函数值为一个非零值。在调用 clearerr(fp)后,
ferror(fp)的值变成 0。
只要出现错误标志,就一直保留,直到对同一文件调用 clearerr函数或 rewind函数,或任何其他一个输入输出函数。
文件这一章的内容是很重要的,许多可供实际使用的 C程序都包含文件处理。本章只介
绍一些最基本的概念,由于篇幅所限,不可能举复杂的例子。希望读者在实践中掌握文件的使用。
习题
13.1 什么是文件型指针?通过文件指针访问文件有什么好处?
13.2 对文件的打开与关闭的含义是什么?为什么要打开和关闭文件?
13.3 从键盘输入一个字符串,将其中的小写字母全部转换成大写字母,然后输出到一个磁盘文件
,test”中保存。输入的字符串以“!”结束。
13.4 有两个磁盘文件,A”和,B”,各存放一行字母,今要求把这两个文件中的信息合并 (按字母顺序排列 ),输出到一个新文件,C”中去。
13.5 有 5个学生,每个学生有 3门课的成绩,从键盘输入以上数据 (包括学生号,姓名,三门课成绩 ),
计算出平均成绩,将原有数据和计算出的平均分数存放在磁盘文件,stud”中。
13.6 将上题,stud”文件中的学生数据,按平均分进行排序处理,将已排序的学生数据存入一个新文件,stu-sort”中。
13.7 将上题已排序的学生成绩文件进行插入处理。
插入一个学生的 3门课成绩,程序先计算新插入学生的平均成绩,然后将它按成绩高低顺序插入,
插入后建立一个新文件。
13.8 上题结果仍存入原有的,stu sort”文件而不另建立新文件。
13.9 有一磁盘文件,emploee”,内存放职工的数据。
每个职工的数据包括职工姓名、职工号、性别、
年龄、住址、工资、健康状况、文化程度。今要求将职工名、工资的信息单独抽出来另建一个简明的职工工资文件。
13.10 从上题的“职工工资文件”中删去一个职工的数据,再存回原文件。
13.1 C文件概述
13.2 文件类型指针
13.3 文件的打开与关闭
13.4 文件的读写
13.5 文件的定位
13.6 出错的检测
13.7 文件输入输出小结习题
13.1 C文件概述
文件 (file)是程序设计中一个重要的概念。所谓,文件,
一般指存储在外部介质上数据的集合。一批数据是以文件的形式存放在外部介质 (如磁盘 )上的。操作系统是以文件为单位对数据进行管理的,也就是说,如果想找存在外部介质上的数据,必须先按文件名找到所指定的文件,然后再从该文件中读取数据。要向外部介质上存储数据也必须先建立一个文件 (以文件名标识 ),才能向它输出数据。
以前各章中所用到的输入和输出,都是以终端为对象的,即从终端键盘输入数据,运行结果输出到终端上。
从操作系统的角度看,每一个与主机相联的输入输出设备都看作是一个文件。例如,终端 键盘是输入文件,
显示屏和打印机是输出文件。
在程序运行时,常常需要将一些数据 (运行的最终结果或中间数据 )输出到磁盘上存放起来,以后需要时再从磁盘中输入到计算机内存。这就要用到磁盘文件。
C语言把文件看作是一个字符 (字节 )的序列,即由一个一个字符 (字节 )的数据顺序组成。根据数据的组织形式,可分为 ASCII文件和二进制文件。
ASCII文件又称文本 (text)文件,它的每一个字节放一个 ASCII代码,代表一个字符。二进制文件是把内存中的数据按其在内存中的存储形式原样输出到磁盘上存放。如果有一个整数 10000,在内存中占 2
个字节,如果按 ASCII码形式输出,则占 5个字节,
而按二进制形式输出,在磁盘上只占 2个字节,见图 13.1。用 ASCII码形式输出与字符一一对应,一个字节代表一个字符,一个字节代表一个字符,
因而便于对字符进行逐个处理,也便于输出字符。
但一般占存储空间较多,而且要花费转换时间 (二进制形式与 ASCII码间的转换 )。用二进制形式输出数值,可以节省外存空间和转换时间,但一个字节并不对应一个字符,不能直接输出字符形式。
一般中间结果数据需要暂时保存在外存上以后又需要输入到内存的,常用二进制文件保存。
图 13.1
由前所述,一个 C文件是一个字节流或二进制流。
它把数据看作是一连串的字符 (字节 ),而不考虑记录的界限。换句话说,C语言中文件并不是由记录 (record)组成的 (这是和 PASCAL或其他高级语言不同的 )。在 C语言中对文件的存取是以字符 (字节 )
为单位的。输入输出的数据流的开始和结束仅受程序控制而不受物理符号 (如回车换行符 )控制。
也就是说,在输出时不会自动增加回车换行符以作为记录结束的标志,输入时不以回车换行符作为记录的间隔 (事实上 C文件并不由记录构成 )。我们把这种文件称为流式文件。 C语言允许对文件存取一个字符,这就增加了处理的灵活性。
在过去使用的 C版本 (如 UNIX系统下使用的 C)有两种对文件的处理方法:一种叫“缓冲文件系统”,
一种叫“非缓冲文件系统”。所谓缓冲文件系统是指系统自动地在内存区为每一个正在使用的文件名开辟一个缓冲区。从内存向磁盘输出数据必须先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘去。如果从磁盘向内存读入数据,则一次从磁盘文件将一批数据输入到内存缓冲区 (充满缓冲区 ),然后再图 13.2
从缓冲区逐个地将数据送到程序数据区 (给程序变量 )。见图 13.2。缓冲区的大小由各个具体的 C版本确定,一般为 512字节。
所谓“非缓冲文件系统”是指系统不自动开辟确定大小的缓冲区,而由程序为每个文件设定缓冲区。
在 UNIX系统下,用缓冲文件系统来处理文本文件,
用非缓冲文件系统处理二进制文件。用缓冲文件系统进行的输入输出又称为高级 (或高层 )磁盘输入输出 (高层 I/O),用非缓冲文件系统进行的输入输出又称为低级 (低层 )输入输出系统。 ANSI C标准决定不采用非缓冲文件系统,而只采用缓冲文件系统。即既用缓冲文件系统处理文本文件,也用它来处理二进制文件。也就是将缓冲文件系统扩充为可以处理二进制文件。
在 C语言中,没有输入输出语句,对文件的读写都是用库函数来实现的。 ANSI规定了标准输入输出函数,用它们对文件进行读写。
本章只介绍 ANSI C规定的文件系统以及对它的读写。
13.2 文件类型指针
缓冲文件系统中,关键的概念是“文件指针”。
每个被使用的文件都在内存中开辟一个区,用来存放文件的有关信息 (如文件的名字、文件状态及文件当前位置等 )。这些信息是保存在一个结构体变量中的。该结构体类型是由系统定义的,取名为 FILE。 Turbo C在 stdio.h文件中有以下的文件类型声明:
typedef struct
{ shortlevel; /*缓冲区“满”或“空”的程度 */
unsignedflags; /*文件状态标志 */
charfd; /*文件描述符 */
unsignedcharhold; /*如无缓冲区不读取字符 */
shortbsize; /*缓冲区的大小 */
unsignedchar*baffer;/*数据缓冲区的位置 */
unsignedar*curp;/*指针,当前的指向 */
unsignedistemp;/*临时文件,指示器 */
shorttoken;/*用于有效性检查 */
} FILE;
有了结构体 FILE类型之后,可以用它来定义若干个 FILE类型的变量,以便存放若干个文件的信息。
例如,可以定义以下 FILE类型的数组。 FILE
f[5];定义了一个结构体数组 f,它有 5个元素,可以用来存放 5个文件的信息。
可以定义文件型指针变量。如,
FILE fp;
fp是一个指向 FILE类型结构体的指针变量。可以使 fp指向某一个文件的结构体变量,从而通过该结构体变量中的文件信息能够访问该文件。也就是说,通过文件指针变量能够找到与它相关的文件。如果有 n个文件,一般应设 n个指针变量 (指向
FILE类型结构体的指针变量 ),使它们分别指向
n个文件 (确切地说指向存放该文件信息的结构体变量 ),以实现对文件的访问。
13.3 文件的打开与关闭
和其他高级语言一样,对文件读写之前应该“打开”该文件,在使用结束之后应关闭该文件。
13.3.1 文件的打开 (fopen函数 )
ANSI C规定了标准输入输出函数库,用 fopen( )
函数来实现打开文件。
fopen函数的调用方式通常为
FILE fp;
fp=fopen(文件名,使用文件方式 );
例如:
fp=fopen("a1","r");
它表示要打开名字为 a1的文件,使用文件方式为
“读入” (r代表 read,即读入 ),fopen函数带回指向 a1文件的指针并赋给 fp,这样 fp就和文件 a1相联系了,或者说,fp指向 a1文件。可以看出,在打开一个文件时,通知给编译系统以下 3个信息:
①需要打开的文件名,也就是准备访问的文件的名字。
② 使用文件的方式 (“读”还是“写”等 )。
③让哪一个指针变量指向被打开的文件。
说明:
(1) 用,r”方式打开的文件只能用于向计算机输入而不能用作向该文件输出数据,而且该文件应该已经存在,不能用,r”方式打开一个并不存在的文件 (即输入文件 ),否则出错。
(2) 用,w”方式打开的文件只能用于向该文件写数据 (即输出文件 ),而不能用来向计算机输入。如果原来不存在该文件,则在打开时新建立一个以指定的名字命名的文件。如果原来已存在一个以该文件名命名的文件,则在打开时将该文件删去,
然后重新建立一个新文件。
(3) 如果希望向文件末尾添加新的数据 (不希望删除原有数据 ),则应该用,a”方式打开。但此时该文件必须已存在,否则将得到出错信息。打开时,
位置指针移到文件末尾。
(4) 用,r+”、,w+”、,a+”方式打开的文件既可以用来输入数据,也可以用来输出数据。用,r+”
方式时该文件应该已经存在,以便能向计算机输入数据。用,w+”方式则新建立一个文件,先向此文件写数据,然后可以读此文件中的数据。用
,a+”方式打开的文件,原来的文件不被删去,位置指针移到文件末尾,可以添加,也可以读。
(5) 如果不能实现“打开”的任务,fopen函数将会带回一个出错信息。出错的原因可能是用,r”
方式打开一个并不存在的文件 ;磁盘出故障 ;磁盘已满无法建立新文件等。此时 fopen函数将带回一个空指针值 NULL(NULL在 stdio,h文件中已被定义为
0)。
常用下面的方法打开一个文件:
if ((fp=fopen("file1","r"))==NULL)
{ printf("cannot open this file\n");
exit(0);
}
即先检查打开的操作有否出错,如果有错就在终端上输出,cannot open this file”。 exit函数的作用是关闭所有文件,终止正在调用的过程。待用户检查出错误,修改后再运行。
(6) 用以上方式可以打开文本文件或二进制文件,
这是 ANSI C的规定,用同一种缓冲文件系统来处理文本文件和二进制文件。但目前使用的有些 C
编译系统可能不完全提供所有这些功能 (例如有的只能用,r”、,w”、,a”方式 ),有的 C版本不用
,r+”、,w+”、,a+”,而用,rw”、,wr”、
,ar”等,请读者注意所用系统的规定。
(7) 在向计算机输入文本文件时,将回车换行符转换为一个换行符,在输出时把换行符转换成为回车和换行两个字符。在用二进制文件时,不进行这种转换,在内存中的数据形式与输出到外部文件中的数据形式完全一致,一一对应。
(8) 在程序开始运行时,系统自动打开 3个标准文件:标准输入、标准输出、标准出错输出。通常这 3个文件都与终端相联系。因此以前我们所用到的从终端输入或输出都不需要打开终端文件。系统自动定义了 3个文件指针 stdin,stdout和 stderr,
分别指向终端输入、终端输出和标准出错输?也从终端输出 )。如果程序中指定要从 stdin所指的文件输入数据,就是指从终端键盘输入数据。
13.3.2 文件的关闭 (fclose函数 )
在使用完一个文件后应该关闭它,以防止它再被误用。“关闭”就是使文件指针变量不指向该文件,
也就是文件指针变量与文件“脱钩”,此后不能再通过该指针对原来与其相联系的文件进行读写操作。除非再次打开,使该指针变量重新指向该文件。
用 fclose函数关闭文件。 fclose函数调用的一般形式为
fclose(文件指针 );
例如:
fclose(fp);
前面我们曾把打开文件 (用 fopen函数 )时所带回的指针赋给了 fp,今通过 fp把该文件关闭。即 fp不再指向该文件。
应该养成在程序终止之前关闭所有文件的习惯,
如果不关闭文件将会丢失数据。因为,如前所述,
在向文件写数据时,是先将数据输到缓冲区,待缓冲区充满后才正式输出给文件。如果当数据未充满缓冲区而程序结束运行,就会将缓冲区中的数据丢失。用 fclose函数关闭文件,可以避免这个问题,它先把缓冲区中的数据输出到磁盘文件,
然后才释放文件指针变量。
fclose函数也带回一个值,当顺利地执行了关闭操作,则返回值为 0;否则返回 EOF(-1)。可以用
ferror函数来测试 (见 13.6,1节 )。
13.4 文 件 的 读 写
文件打开之后,就可以对它进行读写了。常用的读写函数如下所述。
13.4.1 fputc函数和 fgetc函数 (putc函数和 getc函数 )
1,fputc函数
把一个字符写到磁盘文件上去。其一般调用形式为
fputc(ch,fp);
其中 ch是要输出的字符,它可以是一个字符常量,
也可以是一个字符变量。 fp是文件指针变量。
fputc(ch,fp)函数的作用是将字符 (ch的值 )输出到
fp所指向的文件中去。 fputc函数也带回一个值:
如果输出成功则返回值就是输出的字符 ;如果输出失败,则返回一个 EOF(-1)。 EOF是在 stdio,h文件中定义的符号常量,值为 -1。在第 4章介绍过
putchar函数,其实 putchar是从 fputc函数派生出来的。 putchar(c)是在 stdio.h文件中用预处理命令
#define定义的宏,#define putchar(c) fputc(c,
stdout)前面已叙述,stdout是系统定义的文件指针变量,它与终端输出相连。 fputc(c瑂 tdout)的作用是将 c的值输出到终端。用宏 putchar(c)比写
fputc(c,stdout)简单一些。从用户的角度,可以把 putchar(c)看作函数而不必严格地称它为宏。
2,fgetc函数
从指定的文件读入一个字符,该文件必须是以读或读写方式打开的。 fgetc函数的调用形式为
ch=fgetc(fp);
fp为文件型指针变量,ch为字符变量。 fgetc函数带回一个字符,赋给 ch。如果在执行 fgetc函数读字符时遇到文件结束符,函数返回一个文件结束标志 EOF(-1)。如果想从一个磁盘文件顺序读入字符并在屏幕上显示出来,可以:
ch=fgetc(fp);
while(ch! =EOF)
{ putchar(ch);
ch=fgetc(fp);
}
注意,EOF不是可输出字符,因此不能在屏幕上显示。由于字符的 ASCII码不可能出现 -1,因此 EOF
定义为 -1是合适的。当读入的字符值等于 -1(即 EOF)
时,表示读入的已不是正常的字符而是文件结束符。
但以上只适用于读文本文件的情况。现在 ANSI C
已允许用缓冲文件系统处理二进制文件,而读入某一个字节中的二进制数据的值有可能是 -1,而这又恰好是 EOF的值。这就出现了需要读入有用数据而却被处理为“文件结束”的情况。为了解决这个问题,ANSI C提供一个 feof函数来判断文件是否真的结束。 feof(fp)用来测试 fp所指向的文件当前状态是否“文件结束”。如果是文件结束,函数 feof(fp)的值为 1(真 ),否则为 0(假 )。
如果想顺序读入一个二进制文件中的数据,可以用
while(! feof(fp))
{ c=fgetc(fp);
…
}
当未遇文件结束,feof(fp)的值为 0,! feof(fp)为 1,
读入一个字节的数据赋给整型变量 c,并接着对其进行所需的处理。直到遇文件结束,feof(fp)值为
1,! feof(fp)值为 0,不再执行 while循环。
这种方法也适用于文本文件。
3,fputc和 fgetc函数使用举例
在掌握了以上几种函数以后,可以编制一些简单的使用文件的程序。
例 13.1从键盘输入一些字符,逐个把它们送到磁盘上去,直到输入一个,#”为止。
#include <stdio,h>
main( )
{ FILE fp;
char ch,filename[10];
scanf("%s",filename);
if((fp=fopen(filename,"w"))==NULL)
{ printf("cannot open file\n");
exit(0);}
ch=getchar( );/*此语句用来接收在执行
scanf语句时最后输入的回车符 */
ch=getchar( );/*接收输入的第一个字符 */
while(ch! ='#')
{
fputc(ch,fp);putchar(ch);
ch=getchar();
}
fclose(fp);
}
运行情况如下:
file1,c (输入磁盘文件名 )
computer and c# (输入一个字符串 )
computer and c (输出一个字符串 )
文件名由键盘输入,赋给字符数组 filename,
fopen函数中的第一个参数“文件名”可以直接写成字符串常量形式 (如,file1,c”),也可以用字符数组名,在字符数组中存放文件名 (如本例所用的方法 )。本例运行时,从键盘输入磁盘文件名
,file1,c”,然后输入要写入该磁盘文件的字符
,computer and c”,,#”是表示输入结束,程序将,computer and c”写到以,file1,c”命名的磁盘文件中,同时在屏幕上显示这些字符,以便核对。
可以用 DOS命令将 file1,c文件中的内容打印出来:
C> type file1,c
computer and c
证明了在 file1,c文件中已存入了,computer and
c”的信息。
例 13.2将一个磁盘文件中的信息复制到另一个磁盘文件中。
#include <stdio,h>
main( )
{ FILE in out;
char ch,infile[10],outfile[10];
printf("Enter the infile name,\n");
scanf("%s",infile);
printf("Enter the outfile name,\n");
scanf("%s",outfile);
if((in=fopen(infile,"r"))==NULL)
{ printf("cannot open infile\n");
exit(0);
}
if((out=fopen(outfile,"w"))==NULL)
{ printf("cannot open outfile\n");
exit(0);
}
while(! feof(in))fputc(fgetc(in),out);
fclose(in);
fclose(out);
}
运行情况如下:
Enter the infile name:
file1,c (输入原有磁盘文件名 )
Enter the outfile name:
file2,c (输入新复制的磁盘文件名 )
程序运行结果是将 file1,c文件中的内容复制到
file2,c中去。可以用下面 DOS命令验证
c> type file1,c
computer and c(file1,c中的信息 )
c> type file2,c
computer and c (file2,c中的信息 )
以上程序是按文本文件方式处理的。也可以用此程序来复制一个二进制文件,只需将两个 fopen函数中的,r”和,w”分别改为,rb”和,wb”即可。
也可以在输入命令行时把两个文件名一起输入。
这时要用到 main函数的参数。程序可改为
#include <stdio,h>
main(int argc,char*argv[ ])
{ FILE in,*out;
char ch;
if (argc! =3)
{ printf("You forgot to enter a filename\n");
exit(0);
}
if((in=fopen(argv[1],"r"))==NULL)
{ printf("cannot open infile\n");
exit(0);
}
if((out=fopen(argv[2],"w"))==NULL)
{ printf("cannot open outfile\n");
exit(0);
}
while(!feof(in)) fputc(fgetc(in),out);
fclose(in);
fclose(out);
}
假若本程序的源文件名为 a.c,经编译连接后得到的可执行文件名为 a.exe,则在 DOS命令工作方式下,可以输入以下的命令行:
C> a file1,c file2,c
即在键入可执行文件名后,再输入两个参数
file1,c和 file2,c,分别输入到 argv[1]和 argv[2]
中,argv[0]的内容为 a,argc的值等于 3(因为此命令行共有 3个参数 )。如果输入的参数少于 3个,则程序会输出:“你忘了输入一个文件名”。程序执行结果是将 file1,c中的信息复制到 file2,c中。
可以用以下命令验证:
C> type file1,c
computer and c
(这是 file1,c文件中的信息 )
C> type file2,c
computer and c
(这是 file2,c文件中的信息。可见 file1,c已复制到 file2,c中了 )。
最后说明一点,为了书写方便,系统把 fputc和
fgetc定义为宏名 putc和 getc:
#define putc(ch,fp) fputc(ch,fp)
#define getc(fp) fgetc(fp)
这是在 stdio,h中定义的。因此,用 putc和 fputc及用 getc和 fgetc是一样的。一般可以把它们作为相同的函数来对待。
13.4.2 fread函数和 fwrite函数
用 getc和 putc函数可以用来读写文件中的一个字符。
但是常常要求一次读入一组数据 (例如,一个实数或一个结构体变量的值 ),ANSI C标准提出设置两个函数 (fread和 fwrite),用来读写一个数据块。它们的一般调用形式为
fread(buffer,siz e,count,fp);
fwrite(buffer,siz e,count,fp);
其中:
buffer:是一个指针。对 fread来说,它是读入数据的存放地址。对 fwrite来说,是要输出数据的地址 (以上指的是起始地址 )。
size:要读写的字节数。
count:要进行读写多少个 siz e字节的数据项。
fp:文件型指针。
如果文件以二进制形式打开,用 fread和
fwrite函数就可以读写任何类型的信息,如:
fread(f,4,2,fp);
其中 f是一个实型数组名。一个实型变量占 4个字节。
这个函数从 fp所指向的文件读入 2次 (每次 4个字节 )
数据,存储到数组 f中。
如果有一个如下的结构体类型:
struct student-type
{ char name[10];
int num;
int age;
char addr[30];
} stud[40];
结构体数组 stud有 40个元素,每一个元素用来存放一个学生的数据 (包括姓名、学号、年龄、地址 )。
假设学生的数据已存放在磁盘文件中,可以用下面的 for语句和 fread函数读入 40个学生的数据:
for(i=0;i< 40;i++)
fread(&stud[i],sizeof(struct student-type),1,fp);
同样,以下 for语句和 fwrite函数可以将内存中的学生数据输出到磁盘文件中去:
for(i=0;i< 40,i++)
fwrite(&stud[i],sizeof(struct student-type),1,fp);
如果 fread或 fwrite调用成功,则函数返回值为
count的值,即输入或输出数据项的完整个数。
下面写出一个完整的程序。
例 13.3从键盘输入 4个学生的有关数据,然后把它们转存到磁盘文件上去。
#include <stdio,h>
#define SIZE 4
struct student-type
{ char name[10];
int num;
int age;
char addr[15];
} stud[SIZE];
void save( )
{ FILE fp;
int i;
if((fp=fopen("stu-list","wb"))==NULL)
{ printf("cannot open file\n");
return;
}
for(i=0;i< SIZE;i++)
if(fwrite(&stud[i],sizeof(struct student-
type),1,fp)!=1)
printf("file write error\n");
fclose (fp);
}
main()
{ int i;
for(i=0;i< SIZE;i++)
scanf("%s%d%d%s",stud[i].name,
&stud[i].num,&stud[i].age,stud[i].addr);
save( );
}
在 main函数中,从终端键盘输入 4个学生的数据,
然后调用 save函数,将这些数据输出到以
,stu_list”命名的磁盘文件中。 fwrite函数的作用是将一个长度为 29字节的数据块送到 stu_list文件中 (一个 student_type类型结构体变量的长度为它的成员长度之和,即 10+2+2+15=29)。运行情况如下:
输入 4个学生的姓名、学号、年龄和地址:
Zhang 1001 19 room-101
Fun 1002 20 room-102
Tan 1003 21 room-103
Ling 1004 21 room-104
程序运行时,屏幕上并无输出任何信息,只是将从键盘输入的数据送到磁盘文件上。为了验证在磁盘文件,stu_list”中是否已存在此数据可以用以下程序从,stu_list”文件中读入数据,然后在屏幕上输出。
#include <stdio,h>
#define SIZE 4
struct student-type
{ char name[10];
int num;
int age;
char addr[15];
} stud[SIZE];
main( )
{ int i;
FILE fp;
fp=fopen("stu-list","rb");
for(i=0;i< SIZE;i++)
{ fread(&stud[i],sizeof(struct student-type),1,
fp);
printf("%-10s %4d %4d %-15s\n",
stud[i].name,&stud[i].num,&stud[i],age,
stud[i].addr);
fclose (fp);
}
程序运行时不需从键盘输入任何数据。屏幕上显示出以下信息:
Zhang100119room-101
Fun 1002 20 room-102
Tan 1003 21 room-103
Ling 1004 21 room-104
请注意输入输出数据的状况。从键盘输入 4个学生的数据是 ASCII码,也就是文本文件。在送到计算机内存时,回车和换行符转换成一个换行符。再从内存以,wb”方式
(二进制写 )输出到,stu_list”文件,此时不发生字符转换,按内存中存储形式原样输出到磁盘文件上。
在上面验证程序中,又用 fread函数从,stu_list”文件向内存读入数据,注意此时用的是,rb”方式,
即二进制方式,数据按原样输入,也不发生字符转换。也就是这时候内存中的数据恢复到第一个程序向
,stu-list”输出以前的情况。最后在验证程序中,
用 printf函数输出到屏幕,printf是格式输出函数,
输出 ASCII码,在屏幕上显示字符。换行符又转换为回车加换行符。
如果企图从,stu_list”文件中以,r”方式读入数据就会出错。
fread和 fwrite函数一般用于二进制文件的输入输出。因为它们是按数据块的长度来处理输入输出的,在字符发生转换的情况下很可能出现与原设想的情况不同。
例如,如果写 fread(&stud[i],sizeof(struct
student-type),1,stdin);
企图从终端键盘输入数据,这在语法上并不存在错误,编译能通过。如果用以下形式输入数据:
Zhang 1001 10 room-101
…
由于 fread函数要求一次输入 29个字节 (而不问这些字节的内容 ),因此输入数据中的空格也作为输入数据而不作为数据间的分隔符了。连空格也存储到 stud[i]中了,显然是不对的。
这个题目要求的是从键盘输入数据,如果已有的数据已以二进制形式存储在一个磁盘文件,stu-
dat”中,要求从其中读入数据并输出到,stu-list”
文件中,可以编写一个 load函数,从磁盘文件中读二进制数据。
void load( )
{ FILE fp;
int i;
if((fp=fopen("stu-dat","rb"))==NULL)
{ printf("cannot open infile\n");
return;}
for(i=0;i< SIZE;i++)
if(fread(&stud[i],sizeof(struct student-type),
1,fp)!=1)
{ if(feof(fp)){fclose (fp); return;}
printf("file read error\n");
}
fclose (fp);
}
将 load函数加到本题原来的程序文件中,并将
main函数改为 main( )
{
load( );
save( );
}
13.4.3 fprintf函数和 fscanf函数
fprintf函数,fscanf函数与 printf函数,scanf函数作用相仿,都是格式化读写函数。只有一点不同:
fprintf和 fscanf函数的读写对象不是终端而是磁盘文件。它们的一般调用方式为 fprintf(文件指针,
格式字符串,输出表列 );fscanf (文件指针,格式字符串,输入表列 );例如,
fprintf(fp,"%d,%6,2f",i,t);
它的作用是将整型变量 i和实型变量 t的值按 %d和
%6,2f的格式输出到 fp指向的文件上。如果 i=3,
t=4,5,则输出到磁盘文件上的是以下的字符串:
3,4,50
同样,用以下 fscanf函数可以从磁盘文件上读入
ASCII字符,fscanf(fp,"%d,%f",&i,&t);
磁盘文件上如果有以下字符:
3,4,5
则将磁盘文件中的数据 3送给变量 i,4,5送给变量 t。
用 fprintf和 fscanf函数对磁盘文件读写,使用方便,
容易理解,但由于在输入时要将 ASCII码转换为二进制形式,在输出时又要将二进制形式转换成字符,花费时间比较多。因此,在内存与磁盘频繁交换数据的情况下,最好不用 fprintf和
fscanf函数,而用 fread和 fwrite函数。
13.4.4 其他读写函数
1,putw和 getw函数
大多数 C编译系统都提供另外两个函数,putw和
getw,用来对磁盘文件读写一个字 (整数 )。例如:
putw(10,fp);
它的作用是将整数 10输出到 fp指向的文件。而
i=getw(fp);的作用是从磁盘文件读一个整数到内存,赋给整型变量 i。
如果所用的 C编译的库函数中不包括 putw和 getw
函数,可以自己定义该两函数。 putw函数如下:
putw(int i,FILE *fp)
{ char s;
s=(char*)&i;
putc(s[0],fp);putc(s[1],fp);
return(i);
}
当调用 putw函数时,如果用,putw(10,fp);”语句,形参 i得到实参传来的值 10,在 putw函数中将 i
的地址赋予指针变量 s,而 s是指向字符变量的指针变量,因此 s指向 i的第 1个字节,s+1指向 i的第 2
个字节。由于 *(s+0)就是 s[0],*(s+1)就是 s[1],因此,s[0],s[1]分别对应 i的第 1字节和第 2个字节。
由于图 13.3
*(s+0)就是 s[0],*(s+1)就是 s[1],因此,s[0],s[1]
分别对应 i的第 1字节和第 2字节。顺序输出 s[0]、
s[1]就相当于输出了 i的两个字节中的内容。见图
13.3。
getw函数如下:
getw(FILE *fp)
{ char s;
int i;
s=(char *)&i; /*使 s指向 i的起始地址 */
s[0]=getc(fp);
s[1]=getc(fp);
return(i);
}
putw和 getw并不是 ANSI C标准定义的函数。但许多 C编译都提供这两个函数,但有的 C编译可能不以 putw和 getw命名此两函数,而用其他函数名,
请用时注意。
2,读写其他类型数据
如果用 ANSI C提供的 fread和 fwrite函数,读写任何类型数据都是十分方便的。如果所用的系统不提供这两个函数,用户只好自己定义所需函数。
例如,可以定义一个向磁盘文件写一个实数 (用二进制方式 )的函数 putfloat:
putfloat(float num,FILE *fp)
{ char s;
int count;
s=(char *)#
for(count=0;count< 4;count++)
putc(s[count],fp);
}
同样可以编写出读写任何类型数据的函数。
3,fgets函数和 fputs函数
fgets的作用是从指定文件读入一个字符串。如,
fgets(str,n,fp);
n为要求得到的字符,但只从 fp指向的文件输入 n-
1个字符,然后在最后加一个‘ \0’字符,因此得到的字符串共有 n个字符。把它们放到字符数组 str
中。如果在读完 n-1个字符之前遇到换行符或 EOF,
读入即结束。 fgets函数返回值为 str的首地址。
fputs函数的作用是向指定的文件输出一个字符串。
如,fputs("China",fp);把字符串,China”输出到
fp指向的文件。 fputs函数中第一个参数可以是字符串常量、字符数组名或字符型指针。字符串末尾的‘ \0’ 不输出。若输出成功,函数值为 0;失败时,为 EOF。
这两个函数类似以前介绍过的 gets和 puts函数,只是 fgets和 fputs函数以指定的文件作为读写对象。
13.5 文 件 的 定 位
文件中有一个位置指针,指向当前读写的位置。
如果顺序读写一个文件,每次读写一个字符,则读写完一个字符后,该位置指针自动移动指向下一个字符位置。如果想改变这样的规律,强制使位置指针指向其他指定的位置,可以用有关函数。
13.5.1 rewind函数
rewind函数的作用是使位置指针重新返回文件的开头。此函数没有返回值。
例 13.4有一个磁盘文件,第一次将它的内容显示在屏幕上,第二次把它复制到另一文件上。
#include<stdio.h>
main()
{ FILE fp1,*fp2;
fp1=fopen("file1,c","r");
fp2=fopen("file2,c","w");
while(! feof(fp1)) putchar(getc(fp1));
rewind(fp1);
while(! feof(fp1)) putc(getc(fp1),fp2);
fclose(fp1);fclose(fp2);
}
在第一次将文件的内容显示在屏幕以后,文件
file1,c的位置指针已指到文件末尾,feof的值为非零 (真 )。执行 rewind函数,使文件的位置指针重新定位于文件开头,并使 feof函数的值恢复为
0(假 )。
13.5.2 fseek函数和随机读写
对流式文件可以进行顺序读写,也可以进行随机读写。关键在于控制文件的位置指针,如果位置指针是按字节位置顺序移动的,就是顺序读写。
如果能将位置指针按需要移动到任意位置,就可以实现随机读写。所谓随机读写,是指读写完上一个字符 (字节 )后,并不一定要读写其后续的址?
字节 ),而可以读写文件中任意所需的字符 (字节 )。
用 fseek函数可以实现改变文件的位置指针。
fseek函数的调用形式为 fseek (文件类型指针,位移量,起始点 )
,起始点”用 0,1或 2代替,0代表“文件开始”,
1为“当前位置”,2为“文件末尾”。 ANSI C标准指定的名字如表 13.2所示。
,位移量”指以“起始点”为基点,向前移动的字节数。 ANSI C和大多数 C版本要求位移量是
long型数据。这样当文件的长度大于 64K时不致出问题。 ANSI C标准规定在数字的末尾加一个字母 L,就表示是 long型。
下面是 fseek函数调用的几个例子:
fseek(fp,100L,0);将位置指针移到离文件头 100
个字节处
fseek(fp,50L,1); 将位置指针移到离当前位置
50个字节处
fseek(fp,-10L,2); 将位置指针从文件末尾处向后退 10个字节
利用 fseek函数就可以实现随机读写了。
例 13.5在磁盘文件上存有 10个学生的数据。要求将第 1,3,5,7,9个学生数据输入计算机,并在屏幕上显示出来。
程序如下:
#include<stdio.h>
struct student-type
{
char name[10];
int num;
int age;
char sex;
} stud[10];
main()
{
int i;
FILE *fp;
if((fp=fopen("stud -dat","rb"))==NULL)
{printf("can not open file\n");
exit(0);}
for(i=0;i< 10;i+=2)
{
fseek(fp,i*sizeof(struct student-type),0);
fread(&stud[i],sizeof(struct student-type),1,
fp);
printf("%s %d %d %c\n",stud[i].name,
stud[i].num,stud[i].age,stud[i].sex);
}
fclose(fp);
}
13.5.3 ftell函数
ftell函数的作用是得到流式文件中的当前位置,用相对于文件开头的位移量来表示。由于文件中的位置指针经常移动,人们往往不容易知道其当前位置。用 ftell函数可以得到当前位置。如果 ftell函数返回值为 -1L,表示出错。例如:
i=ftell(fp);
if(i==-1L)printf("error\n");
变量 i存放当前位置,如调用函数出错 (如不存在此文件 ),则输出,error”。
13.6 出 错 的 检 测
C标准提供一些函数用来检查输入输出函数调用中的错误。
13.6.1 ferror函数
在调用各种输入输出函数 (如 putc,getc,fread、
fwrite等 )时,如果出现错误,除了函数返回值有所反映外,还可以用 ferror函数检查。它的一般调用形式为 ferror(fp);
如果 ferror返回值为 0(假 ),表示未出错。如果返回一个非零值,表示出错。应该注意,对同一个文件每一次调用输入输出函数,均产生一个新的
ferror函数值,因此,应当在调用一个输入输出函数后立即检查 ferror函数的值,否则信息会丢失。
在执行 fopen函数时,ferror?。
13.6.2 clearerr函数
它的作用是使文件错误标志和文件结束标志置为 0。
假设在调用一个输入输出函数时出现错误,ferror
函数值为一个非零值。在调用 clearerr(fp)后,
ferror(fp)的值变成 0。
只要出现错误标志,就一直保留,直到对同一文件调用 clearerr函数或 rewind函数,或任何其他一个输入输出函数。
文件这一章的内容是很重要的,许多可供实际使用的 C程序都包含文件处理。本章只介
绍一些最基本的概念,由于篇幅所限,不可能举复杂的例子。希望读者在实践中掌握文件的使用。
习题
13.1 什么是文件型指针?通过文件指针访问文件有什么好处?
13.2 对文件的打开与关闭的含义是什么?为什么要打开和关闭文件?
13.3 从键盘输入一个字符串,将其中的小写字母全部转换成大写字母,然后输出到一个磁盘文件
,test”中保存。输入的字符串以“!”结束。
13.4 有两个磁盘文件,A”和,B”,各存放一行字母,今要求把这两个文件中的信息合并 (按字母顺序排列 ),输出到一个新文件,C”中去。
13.5 有 5个学生,每个学生有 3门课的成绩,从键盘输入以上数据 (包括学生号,姓名,三门课成绩 ),
计算出平均成绩,将原有数据和计算出的平均分数存放在磁盘文件,stud”中。
13.6 将上题,stud”文件中的学生数据,按平均分进行排序处理,将已排序的学生数据存入一个新文件,stu-sort”中。
13.7 将上题已排序的学生成绩文件进行插入处理。
插入一个学生的 3门课成绩,程序先计算新插入学生的平均成绩,然后将它按成绩高低顺序插入,
插入后建立一个新文件。
13.8 上题结果仍存入原有的,stu sort”文件而不另建立新文件。
13.9 有一磁盘文件,emploee”,内存放职工的数据。
每个职工的数据包括职工姓名、职工号、性别、
年龄、住址、工资、健康状况、文化程度。今要求将职工名、工资的信息单独抽出来另建一个简明的职工工资文件。
13.10 从上题的“职工工资文件”中删去一个职工的数据,再存回原文件。