目录
fputc函数和fgetc函数(putc函数和getc函数)
十三、文件
13.1C文件概述
文件(file)是程序设计中一个重要的概念。所谓“文件”一般指存储在外部介质上数据的集合。一批数据是以文件的形式存放在外部介质(如磁盘)上的。操作系统是以文件为单位对数据进行管理的,也就是说,如果想找存在外部介质上的数据,必须先按文件名找到所指定的文件,然后再从该文件中读取数据。要向外部介质上存储数据也必须先建立一个文件(以文件名标识),才能向它输出数据。
前面我们所用到的输入和输出,都是以终端为对象的,即从终端键盘输入护具,运行结果输出在终端上。从操作系统的角度看,每一个与主机相联的输入输出设备都看作是一个文件。例如:终端键盘是输入文件,显示屏和打印机是输出文件。
在程序运行时,常常需要将一些数据(运行的最终结果或中间数据)输出到磁盘上存放起来,以后需要时再从磁盘中输入到计算机内存。这就要用到磁盘文件。
C语言把文件看作是一个字符(字节)的序列,即由一个一个字符(字节)的数据顺序组成的。根据数据的形式,可分为ASCII文件和二进制文件。ASCII文件又称文本(text)文件,它的每一个字节放一个ASCII代码,代表一个字符。二进制文件时把内存中的数据按其在内存中的存储形式原样输出到磁盘存放。如果有一个整数10 000 ,在内存中占2个字节,如果按ASCII码形式输出,则占5个字节,而按二进制形式输出,在磁盘上只占2个字节,见下图。用ASCII码形式输出与字符一一对应,一个字节代表一个字符,因而便于对字符进行逐个处理, 也便于输出字符。但一般占存储空间较多,而且要花费转换时间(二进制形式与ASCII码间的转换)。用二进制形式输出数值,可以节省外存空间和转换时间,但一个字节并不对应一个字符,不能直接输出字符形式。一般中间用结果数据需要暂时保存在外存上,以后又需要输入到内存中,常用二进制文件保存。
由前所述,一个C文件是一个字节流或二进制流。它把数据看作是一连串的字符(字节),而不考虑记录的界限。换句话说,C语言中文件并不是由记录(record)组成的(这是和PASCAL或其他高级语言不同的)。在C语言中对文件的存取是以字符(字节)为单位的。
输入输出的数据流的开始和结束仅受程序控制而不受物理符号(如回车换行符)控制。也就是说,在输出时不会自动增加回车换行符以作为记录的标志,输入时不以回车换行符作为记录的间隔(事实上C文件并不由记录构成)。把这种文件成为流式文件。C语言允许对文件存取一个字符,这就增加了处理的灵活性。
在过去使用的C版本(如UNIX系统下使用的C)有两种对文件的处理方法:一种叫“缓冲文件系统”,一种叫“非缓冲文件系统”。所谓缓冲文件系统是指系统自动地在内存区为每一个正在使用的文件开辟一个缓冲区。从内存向磁盘输出数据必须先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘去。如果从磁盘向内存读入数据,则一次从磁盘文件将一批数据输入到内存缓冲区(充充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(给程序变量),见下图。缓冲区的大小由各个具体的C版本确定,一般为512字节。
所谓“非缓冲文件系统”是指系统不自动开辟确定大小的缓冲区,而由程序为每个文件设定缓冲区。
在UNIX系统下,用缓冲文件系统来处理文本文件,用非缓冲文件系统处理二进制文件。用缓冲文件系统进行的输入输出又称为高级(或高层)磁盘输入输出(高层I/O),用非缓冲文件系统进行的输人输出又称为低级(低层)输入输出系统。ANSI C标准不采用非缓冲文件系统,而只采用缓冲文件,即既用缓冲文件系统处理文本文件,也用它来处理二进制文件,也就是将缓冲文件系统扩充为可以处理二进制文件。
在C语言中,没有输入输出语句,对文件的读写都是用库函数来实现的。ANSI 规定了标准输入输出函数,用它们对文件进行读写。
13.2文件类型指针
缓冲文件系统中,关键的概念是“文件指针”。每个被使用的文件都在内存中开辟一个区,用来存放文件的有关信息(如文件的名字。文件状态及文件的当前位置等)。这些信息室保存在一个结构体变量中的。该结构体类型是由系统定义的,取名为FILE。Turbo C在stdio.h文件中有以下的文件类型声明。
typedef struct {
short level; //缓冲区“满”或“空”的程度
unsigned flags; //文件状态标志
char fd; //文件描述符
unsigned char hold; //如无缓冲区不读取字符
short bsize; //缓冲区的大小
unsigned char *buffer; //数据缓冲区的位置
unsigned ar *curp; //指针,当前的指向
unsigned istemp; //临时文件,指示器
short token; //用于有效性检查
}FILE;
有了结构体FILE类型之后,可以用它来定义若干个FILE类型的变量,以变存放若干个文件的信息。例如,可以定义以下FILE类型的数组:
FILE f[5]; //定义了一个结构体数组f,它有5个元素,可以用来存放5个文件的信息
可以定义文件型指针变量。例如:
FILE *fp;
fp是指向一个FILE类型结构体变量的指针变量。可以使fp指向某一文件的结构体变量,从而通过该结构体变量中的文件信息能够访问该文件。也就是说,通过文件指针变量能够找到与它相关的文件。如果有n个文件,一般应设n个指针变量(指向FILE类型结构体的指针变量),使它们分别指向n个文件(确切地说,指向存放该文件信息的结构体变量),以实现对文件的访问。
13.3文件的打开与关闭
和其他高级语言一样,对文件读写之前应该“打开”该文件,在使用结束之后应“关闭”该文件。
文件的打开(fopen函数)
调用方式为:
FILE *p;
fp = fopen(文件名,使用文件的方式);
例如:
fp = fopen("a1","r");
可以看出,在打开一个文件时,通知编译系统以下3个信息:
-
需要打开的文件名,也就是准备访问的文件的名字
-
使用文件的方式
-
让哪一个指针变量指向被打开的文件
使用文件的方式见下表:
文件使用方式 | 含义 |
---|---|
"r"(只读) | 为输入打开一个文本文件 |
"w"(只写) | 为输出打开一个一个文本文件 |
"a"(追加) | 向文本文件尾添加数据 |
"rb"(只读) | 为输入打开一个二进制文件 |
"wb"(只写) | 为输出打开一个二进制文件 |
"ab"(追加) | 向二进制文件尾添加数据 |
"r+"(读写) | 为读写打开一个文本文件 |
"w+"(读写) | 为读写建立一个新的文本文件 |
"a+"(读写) | 为读写打开一个文本文件 |
"rb+"(读写) | 为读写打开一个二进制文件 |
"wb+"(读写) | 为读写建立一个新的二进制文件 |
"ab+"(读写) | 为读写打开一个二进制文件 |
说明:
(1)用"r"方式打开的文件只能用于向计算机输入而不能用作向该文件输出数据,而且该文件应该已经存在,不能用"r"方式打开一个并不存在的文件,即输入文件;否则出错。
(2)用"w"方式打开的文件只能用于向该文件写数据,即输出文本,而不能用来向计算机输入。如果原来不存在该文件,则在打开时新建立一个以指定名字命名的文件。如果原来已经存在一个以该文件命名的文件,则在打开时将该文件删去,然后重新建立该文件。
(3)如果希望向文本末尾添加新的数据(不希望删除原有数据),则应该用"a"的方式打开。但此时文件必须存在;否则将得到出错信息。打开时,位置指针移到文件末尾。
(4)用"r+"、"w+"、"a+"方式打开的文件既可以用来输入数据,也可以用来输出数据。用"r+"方式时该文件已经存在,以便能向计算机输入数据。用"w+"方式则新建一个文件,先向此文件写数据,然后可以读此文件中的数据。用"a+"方式打开的文件,原来的文件不被删去,位置指针移到文件末尾,可以添加,也可以读。
(5)如果文件不能实现打开的任务,fopen函数将会带回一个出错的信息。出错的原因可能是用"r"方式打开一个并不存在的文件;磁盘已满无法建立新文件等。此时fopen函数将带回一个空指针值NULL。
if ((fp=open("file1","r"))==NULL)
{
printf("cannot open this file \n");
exit(0); //关闭所有文件,终止正在执行的程序,待用户检查出错误,修改后再进行
}
(6)以上方式可以打开文本文件或二进制文件,这是SNSI C的规定,用同一种缓冲文件系统来处理文本文件和二进制文件。
(7)在向计算机输入文本文件时,将回车换行符转换为一个换行符,在输出时把换行符换成为回车和换行两个字符。在用二进制文件时,不进行这种转换,在内存的数据形式与输出到外部文件中的数据形式完全一致,一一对应。
(8)在程序开始运行时,系统自动打开3个标准文件:标准输入、标准输出、标准出错输出。通常这3个文件都与终端相联系。
文件的关闭(fclose函数)
在使用完一个文件后应该关闭它,以防止它再被误用。“关闭”就是使文件指针变量不指向该文件。
fclose调用的一般形式为:
fclose(文件指针);
例如:
fclose(fp);
在程序终止前应该关闭所有文件,如果不关闭文件,则会丢失数据。
13.4文件的读写
fputc函数和fgetc函数(putc函数和getc函数)
fputc函数
把一个字符写到磁盘文件上去。其调用的一般形式为:
fputc(ch,fp);
fgetc函数
从指定的文件读入一个字符,该文件必须是以读或读写方式打开的。其调用的一般形式为:
ch = fgetc(fp);
fputc和fgetc函数使用举例
从键盘输入一些字符,逐个把它们送到磁盘上去,直到输入一个“#”为止。
#include<stdio.h>
#include<stdlib.h>
int main()
{
FILE *fp;
char ch,filename[10];
scanf("%s",filename);
if((fp=fopen(filename,"w"))==NULL)
{
printf("cannot open this file \n");
exit(0); //终止程序
}
ch = getchar(); //此语句用来接收在执行scanf语句时最后输入的回车符
ch = getchar(); //接收输入的第一个字符
while(ch!='#')
{
fputc(ch,fp);
putchar(ch);
ch = getchar();
}
putchar(10); //向屏幕输出一个换行符
fclose(fp);
return 0;
}
//输入
file.c
computer and c#
//输出
computer and c
将一个磁盘文件中的信息复制到另一个磁盘文件中。
#include<stdio.h>
#include<stdlib.h>
int 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);
return 0;
}
以上程序是按文本文件方式处理的。也可以用此程序来复制一个二进制文本,只需要将两个fopen函数中的"r"和"w"分别改成"rb"和"wb"即可。
fread函数和fwrite函数
用fgetc和fputc函数可以用来读写文件中的一个字符。但是常常要求一次读入一组数据(例如,一个实数或一个结构体变量的值),ANSI C标准提出设置两个函数(fread函数和fwrite函数),用来读写一个数据块。它们的一般调用形式为:
fread(buffer,size,count,fp);
fwrite(buffer,count,fp);
其中
buffer:是一个指针。对fread来说,它是读入数据的存放地址。对fwrite来说,是要输出数据的地址(以上指的是起始地址)。
size:要读写的字节数。
count:要进行读写多少个size字节的数据项。
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(int i=0;i<40;i++)
{
fread(&stud[i],sizeof(struct student_type),1,fp);
}
同样,以下for语句和fwrite函数可以将内存中的学生数据输出到磁盘文件中去:
for(int i=0;i<40;i++)
{
fwrite(&stud[i],sizeof(struct student_type),1,fp);
}
如果fread或fwrite调用成功,则函数返回值为count的值,即输入或输出数据项的完整个数。
下面写一个完整的程序。
从键盘输入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);
}
int 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();
}
//输入:
Zhang 1001 19 room_101
Fun 1002 20 room_102
Tan 1003 21 room_103
King 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];
int 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);
}
//输出:
Zhang 1001 19room_101
Fun 1002 20room_102
Tan 1003 21room_103
King 1004 21room_104
请注意输入输出数据的情况。从键盘输入4个学生的数据是ASCII码,也就是文本文件。在送到计算机内存时,回车和换行符转换成一个换行符。再从内存以"wb"方式(二进制)输出到"stu_list"文件,此时不发生字符转化,按内存中存储形式原样输出到磁盘文件上。在上面的验证程序中,又用fread函数从"stu_list"文件向内存肚饿数据,注意此时用的是"rb"方式,即二进制方式,数据按原样输入,也不发生字符转换。也就是这时内存中的数据恢复到第一个程序向"stu_list"输出以前的情况。最后在验证程序中,用printf函数输出到屏幕,printf是格式输出函数,输出ASCII码,在屏幕上显示字符。换行符又转换为回车加换行符。
fprintf函数和fscanf函数
fprintf函数、fscanf函数与printf函数、scanf函数作用相仿,都是格式化读写函数。只有一点不同:fprintf函数和fscanf函数的读写对象不是终端,而是磁盘文件。它们的一般调用格式为:
fprintf(文件指针,格式字符串,输出表列);
fscanf(文件指针,格式字符串,输出表列);
例如:
fprintf(fp,"%d,%6.2f",i,t);
fscanf(fp,"%d %f",&i,&t);
用fprintf和fscanf函数对磁盘文件读写,使用方便,容易理解,但由于在输入时要将ASCII码转换为二进制形式,在输出时又要将二进制形式转换成字符,花费时间较多。因此,在内存与磁盘频繁交换数据的情况下,最好不用fprintf和fscanf函数,而用fread和fwrite函数。
声明:本文章为个人学习笔记,资料整理参考谭浩强《C程序设计(第三版)》如有错误,欢迎大家指正!