C语言文件基础知识
之前学C语言的时候老师讲文件讲的相当潦草,因为大作业的关系现在只能自己重听自学一遍。以下是根据北理工的C语言程序设计mooc的文件单元课件和上课时的补充内容所整理的C语言文件基础知识,希望能对一部分初学者起到帮助
1.文件概述
目前为止我们在程序中使用的数据通常来自于初始化赋值和通过键盘手动录入。在实际应用中数据来源往往涉及外部存储介质。
什么是文件?
文件是存储在外部介质上(如磁盘和磁带等外存储器)数据或信息的集合。程序文件中保存着程序,而数据文件中保存着数据
例如我们编写的文本文件(.c源程序),和编译后形成的二进制文件(.obj目标文件,.exe可执行文件)。常用的文档,音视频文件都是以这种形式存储在磁盘上的
文件的性质
文件是一个有序的数据序列。文件的所有数据之间有着严格的排列次序的关系(类似数组类型的数
据),要访问文件中的数据必须按照它们的排列顺序依次进行访问。例如下图中的C语言源程序,如下图中右侧所示它是由这样一些字母和符号所排列组成的有序集合,通过ASCII码的编码方式将它转化为二进制存储的。
这样的一个程序经过编译链接后得到的可执行程序则存储的是计算机可理解的指令代码与ASCII码不同是我们所不能直接理解的。
系统对文件的处理过程
在计算机中所有运行的程序必须载入到内存中才能正常的运行和处理。因此系统必须有一个过程实现内存与外存的数据交换。通常这样一个过程是通过文件缓冲区来进行的。我们将文件缓冲区中的数据存储在外存上称为写文件或者说文件的输出。将外存中的文件读取到缓冲区中供文件使用叫做读文件或者文件的输入
文件的分类
按文件中数据的组织形式看
根据数据在磁盘上存储的格式,将文件分成两类:
1.文本文件(或称ASCII文件,即txt文件)
文本文件中保存的数据采用ASCII码作为存储方式,先将全部数据转换为ASCII码形式。每个ASCII字符占用一个字节(对它的扩展采用unicode码会占用更多的字节)。
文本文件使用编辑软件可以直接阅读。
2.二进制文件
二进制文件中保存的数据是将数据在内存中二进制存储格式不做任何转换直接存入文件中。
二进制文件使用编辑软件不能直接阅读。
从用户的角度文件可分为:
1.普通文件(也称为磁盘文件)
普通文件是以磁盘为对象且无其它特殊性能的文件,我们所熟悉的存储在磁盘上的一般的数据集合
2.特殊文件(也称为标准设备文件或标准 I/O 文件)
特殊文件是以终端为对象的标准的设备文件
在C语言中,"文件"的概念具有广泛的意义。它把与主机进行数据交换的输入输出设备都看作是一个文件 (例如我们平常使用的打印机显示器和键盘等本质上来说都是产生和消费数据的设备)。即把实际的物理设备抽象为逻辑文件,它们也被称为设备文件
例如: 键盘作为标准输入文件 文件名stdin
显示器作为标准输出文件 文件名是stdout
打印机也作为输出文件 文件名是PRN
从C语言对文件的处理方式看,根据对文件的处理方式可将文件分为两类:
1.缓冲文件系统
对每个正在使用的文件系统自动在内存中为其开辟一个文件缓冲区,程序对文件的处理是通过缓冲区间接进行的。也称为高级文件操作。
2.非缓冲文件系统
系统不会自动开辟文件缓冲区而是由应用程序自己设置(也就是程序员自己编程实现)。也称为低级文件操作。
在83年以后ANSIC标准中取消了非缓冲文件系统,对文本文件和二进制文件均统采用缓冲文件系统进行处理
2.文件指针
C语言的文件读写通常采用缓冲文件系统,也就是说我们不能直接访问硬盘的数据,而要通过打开文件的方式将磁盘文件或者设备文件的数据缓冲到内存中。这个内存数据需要通过FILE文件结构变量进行管理和维护,而维护是由系统内部进行的。留给编程人员的就是一个文件指针及其相应的操作函数
文件结构类型
在缓冲文件系统中对每个正在使用的文件都要使用一个FILE 类型的结构变量来存储相关信息,该结构变量用于存放文件的有关信息,如文件名,文件状态等。在C语言中无论是一般磁盘文件还是设备文件都要通过文件结构的相应成员数据进行输入输出处理。
文件结构不需要用户自己定义,是由系统事先已经定义好的,固定包含在头文件stdio.h中
文件结构FILE的定义,里面包含了我们使用一个缓冲区数据所关心的东西
typedef struct
{
short level; //文件缓冲区中剩余的字节数
unsigned flags; //文件状态标志
char fd; //文件描述符
unsigned char hold; //缓冲区满时丢失的字符
short bsize; //文件缓冲区大小
unsigned char *buffer; //文件缓冲区地址
unsigned char *curp; //文件读写下一个字符位置
unsigned istemp; //临时文件指针
short token; //校验符
}FILE;
文件指针变量
文件结构已由系统定义,它是由系统自动生成的。在C程序后续操作中都要通过指向该文件结构的指针对已打开的文件进行操作。为此需要在程序中对每个要打开的文件定义一个FILE类型(文件型)的指针变量
文件型指针变量说明形式
FILE**文件型指针变量名;
例如:FILE *fp;
fp是一个指针变量,指向文件结构
这样一个指针变量是后面我们所有的需要对文件进行操作的时候的一个重要参数,对于每一个打开的文件都需要有这样一个指针变量指向它的文件结构。要同时使用多个文件时则须有多个不同的文件指针
系统标准设备文件
有一种情况是比较例外的,那就是系统标准设备文件。
标准设备文件是由系统控制,由系统自动打开和关闭。标准设备文件的文件结构的指针变量由系统命名,用户在程序中可直接使用。
C语言中提供了三个标准设备文件的指针:
stdin 标准输入文件(键盘)
stdout 标准输出文件(显示器)
stderr 标准错误输出文件(显示器)
关于设备文件理解的举例
我们看以前遇到过的一个问题
#include<stdio.h>
#include<string.h>
main(){
printf("this is a test\n");
}
程序在很短时间内执行完毕,我们来不及看中间的结果。因此我们可以修改下程序
#include<stdio.h>
#include<string.h>
main(){
printf("this is a test\n");
getchar() //使它运行结束以前要求从键盘输入一个字符,它的作用是实现程序的暂停
}
现在程序运行在getchar()过程中等待我们输入任意字符,按回车以后结束。这是一个很常见的小技巧。
现在再对程序做一定的修改
#include<stdio.h>
#include<string.h>
main(){
printf("this is a test\n");
char ch;
scanf("%c",&ch);
getchar();
}
程序并没有按照我们预期的停留在getchar()等待我们看完结果实现暂停的功能而是直接运行完毕。因为当我们在键盘上输入a+回车以后键盘缓冲区作为一个文件的缓冲区实际上存储了字母a和回车这个ASCII码,将字母a直接赋值给ch,将回车ASCII码赋值给getchar()函数,因此getchar()已经正确的获得了输入就完成了整个程序。
键盘缓冲区实际上就是一个文件,一种特殊的设备文件。因此我们可以用fflush()的方式清除这个文件,清除这个文件对应的文件指针是一个预定义的指针stdin
#include<stdio.h>
#include<string.h>
main(){
printf("this is a test\n");
char ch;
scanf("%c",&ch);
fflush(stdin);
getchar();
}
这时候已经把回车从文件缓冲区中清除,后面执行的getchar()函数仍然等待我们从键盘输入新的内容
使用文件的一般步骤
打开文件——操作文件——关闭文件
打开文件
通过文件指针建立用户程序与文件的联系,为文件开辟文件缓冲区。将磁盘数据载入到内存中供后续操作使用
操作文件(是指对文件的读、写、追加和定位操作)
读操作: 将文件中的数据读入到程序中
写操作: 将程序中的数据写入到文件中
追加操作: 将数据写到文件中原有数据的后面(不覆盖前面的数据)
定位操作: 文件读写位置指针指向给定位置
注:在以上的读写操作中我们都是对它进行顺序的读写,那么顺序访问的过程中我们有一个临时的读写指针。如果我们需要对给定的位置进行操作的话我们就需要定位操作
以上操作其本质都是对文件缓冲区的一个修改,这样的修改并不能及时反映到磁盘文件中,因此在结束操作的时候我们需要关闭文件
关闭文件
切断文件与程序的联系,将文件缓冲区的内容写入磁盘并释放文件指针。
3.文件的打开与关闭
访问文件数据之前我们首先需要打开文件,也就是将磁盘文件的内容缓存到内存中去。由于对文件的使用方法不同,打开文件的操作也不同。
打开文件(fopen函数)
不同类型的文件打开的方式是相同的,都需要fopen函数
fopen函数的调用形式:(已知 FILE * fp;)
fp=fopen(文件,文件使用方式)
参数:
文件名:需要打开的文件名称(字符串)
例如,fopen(“d:\file.txt”,“w”);
fopen(filename,“r”); filename为字符数组名
文件使用方式:是具有特定含义的符号
注:在操作系统中我们知道,一个文件的完整描述应该包括盘符,路径和文件名,它们之间通过斜线进行分割。但是斜线在C语言中则是转义字符的标志。我们如果需要输出一条斜线是通过两条斜线来转义输出的。
函数功能
按指定的文件使用方式打开指定的文件。
若文件打开成功,则返回值为非NULL指针;
若文件打开失败,返回NULL。
文件使用方式
文本文件的三种基本使用方式
“r”: 只读方式
为读方式(输入)打开文本文件。若文件不存在返回NULL。
“w”:只写方式
为写(输出)方式打开文本文件。若文件不存在建立一个新文件;若文件已存在则要将原来的文件清空。
“a”:追加方式
在文本文件的末尾增加数据。若文件已存在则保持原来文件的内容,将新的数据增加到原来数据的后面;若文件不存在则返回NULL。
二进制和文本文件的编码方式不同,因此系统对它们的处理过程也会不一样。因此我们要告诉系统以什么样的方式打开。默认的情况会使用文本方式打开,如果我们要用二进制进行打开就要在原来的打开模式后加一个小写字母b以示区别
二进制文件的三种基本打开方式
“rb” : 只读方式
“wb”: 只写方式
“ab”: 追加方式
假设我们需要打开一个文件,读取其中的内容并在满足一定条件的情况下将其中内容修改并写回到文件中去。这样一种应用包括读写两种方式,那么以上三种方式就不够用了
文件的其他打开方式
“r+”:对文本文件进行读/写操作。若文件不存在则返回NULL;若文件存在内容不会被清空
“w+”:对文本文件进行读/写操作。若文件已经存在则先将文件原先内容清空
“a+”:对文本文件进行读/追加操作。文件内容不会被清空
“rb+”:对二进制文件进行读/写操作
“wb”:对二进制文件进行读/写操作
“ab+”:对二进制文件进行读/追加操作
常见文件打开操作
if((fp=fopen("d:\\file.txt","r"))==NULL){
printf("Can't open file.\n"); //提醒用户不能打开并调用相关的函数进行处理(这里是exit)
exit(0);
}
含义:以只读方式打开D盘根目录下名为“file.txt”的文件。如果文件成功打开则将建立一个缓冲区并将该文件的结构地址返回到变量fp中。如果打开文件失败(文件打开的过程中可能会出现一些难以避免的错误,例如文件不存在,文件路径有误或者文件存在但是被其他程序占用等等,这个时候如果我们不加判断直接用其他文件相关的处理函数进行调用的话就会导致系统错误)fopen函数返回一个空指针。exit( )是一个系统函数,其功能是清空文件缓冲区,关闭文件,终止当前程序运行,返回到操作系统中;参数0表示程序正常退出,1表示出错退出
关闭文件(fclose函数)
(已知 FILE * fp;)
调用格式:
fclose(fp);
fp:已经打开的文件指针,指针不能为空
函数功能
关闭fp指定的文件,切断缓冲区与该文件的联系并释放文件指针,保存好文件。
若文件关闭成功,则返回值为0;若文件关闭失败,返回非0值。
4.文件的顺序读写
在打开文件获取其指针后我们仍然不能对数据进行直接访问,我们需要用函数对它进行读写,现在来学习顺序读写的方法
文件顺序操作
我们已经知道文件是由数据进行有顺序的排列组成的一个集合,在文件打开的过程中我们已经将磁盘上的这样一个集合放到内存缓冲区中。从原理上来说我们可以通过内存地址对它进行随机访问,但是由于文件是这样一个有序的集合,通常情况下我们会设置一个指向文件当前读写地址的指针,然后通过这个指针的位置依次进行访问,对相关的数据进行遍历。这个过程就叫文件的顺序操作
对文件的操作(文件读写)必须按文件中字符的先后顺序进行,只能在操作了第i个字符之后才能操作第i+1个字符。在对文件操作时文件的位置指针由系统自动向后(文件尾方向)移动
进行顺序操作的函数
字符输入输出函数 fgetc fputc
字符串输入输出函数 fgets fputs
格式化输入输出函数 fscanf fprintf
数据块输入输出函数 fread fwrite
字符输入输出函数
输入函数
ch=fgetc(fp); //fp为已经打开的文件的指针
函数功能:
从指定的文件中读取一个字符。即从fp所指向的文件(该文件必须是以读或读写方式打开的)中读取一个字符返回,读取的字符赋给变量ch。正常情况下在这个文件下有一个用于读写的当前指针,读取一个字符就是从当前指针处读取一个字符返回给变量
对于ASCII文件若读取字符时文件已经结束或出错则返回文件结束标记 EOF(对应编码为-1)。因此我们在对文件读取的时候要对读取的内容进行判断以确定是否结束
输出函数
fputc(ch,fp); //ch为需要输出的字符,fp为已经打开的文件指针
函数功能:
将一个字符输出到指定文件中 。即将字符变量ch中的字符输出到fp所指向的文件。若输出操作成功则该函数返回输出的字符;否则返回EOF
例:显示文件的内容
#include<stdio.h>
main(){
FILE *fp; //建立一个文件指针
char filename[20],ch;
printf("Enter filename:");
scanf("%s",filename); //输入文件名
if((fp=fopen(filename,"r"))==NULL){ //打开文件将结果赋值给fp
printf("file open error.\n"); //出错处理
exit(0);
}
while((ch=fgetc(fp))!=EOF){ //从文件读字符直到文件结束
//putchar() 在显示器上当前位置显示对应的ch字符,也可以用下面的fputc
fputc(ch,stdout);
//向一个文件中写入字符,显示器也是一种特殊的文件,它的标准文件指针是stdout。我们通过fputc向stdout这个文件写入字符,与刚才直接使用putchar()输出的效果是一样的,只是这里将显示器作为一个特殊的文件来处理
} //关闭文件
fclose (fp);
}
//EOF为文件结束标记。同时也是读取成功的标志,读取成功不会返回EOF字符
字符串输入输出
输入函数
fgets(s, n, fp)
参数:char s[]; int n; FILE *fp
函数功能:
数组s已经定义,从fp所指向的文件中读取长度不超过n-1(存储的长度而不是字符的个数且字符串最后以\0结尾)个字符(包括空格)的字符串,并将该字符串存到字符数组s中。函数返回值为字符数组s的首地址;若文件结束或出错则返回NULL
具体实践中可能遇到的几种情况
情况1:已读入n-1个字符,s中存入n-1个字符,串尾为\0
情况2:读入字符遇到\n,s中存入实际读入的字符,串尾为\n\0
情况3:读入字符遇到文件尾EOF,s中存入实际读入的字符,EOF 不会存入数组,串尾为\0
情况4:当文件已经结束时继续读文件,函数的返回值为 NULL,表示文件结束
输出函数
fputs(s,fp);
参数:char s[];FILE *fp;
函数功能:将字符数组s中的字符串写入文件指针f所指的文件中。输入成功则返回值为0
注:第一个参数可以是字符串常量,字符数组名或字符型指针,输出史字符串末尾的\0不会输出
用法示例:
main(){
FILE *fp;
char *c1="Turbo C";
char c2[10];
fp=fopen("d:\\file2.dat","w");
fputs(c1,fp);
fclose(fp);
fp=fopen("d:\\file2.dat","r");
fgets(c2,8,fp);
printf("%s\n",c2);
fcolse(fp);
}
格式化输入输出
输入函数
fscanf(fp,格式控制符,变量地址表);
fscanf(fp,格式控制符,变量地址表);
参数:FILE *fp;
函数功能:从fp所指向的ASCII文件中读取字符,按格式控制符的含义存入对应的变量中,返回值为输入的数据个数。fscanf与scanf类似,格式控制符相同 。
输出函数
fprintf(fp,格式控制符,表达式列表)
参数:FILE *fp;
函数功能:将表达式列表中的数据按照格式控制符的说明存入fp所指向的ASCII文件中,返回值为实际存入的数据个数。fprintf与printf类似,格式控制符相同。
二者加f表示面向文件操作,fp为指向要操作文件的指针
前面的函数都面向文本文件读写,数据块输入输出函数则面向二进制文件读写
数据块输入输出函数
输入函数
fread(buffer,size,count,fp);
参数:char *buffer;unsigned size;int count;FILE *fp;
函数功能:
从二进制文件fp中读取count个数据块存入buffer中,每个数据块的大小为 size 个字节。操作成功函数的返回值为实际读入的数据块的数量;若文件结束或出错返回值为0。
输出函数
fwrite(buffer,size,count,fp);
参数:char *buffer;unsigned size;int count;FILE *fp;
函数功能:
将buffer中的count个数据块写入二进制文件fp中,每个数据块的大小为size个字节。操作成功,函数的返回值为实际写入文件的数据块的数量;若文件结束或出错,返回值为0
用法示例:输入3个同学的信息并保存在D盘student目录下
#include<stdio.h>
#define SIZE 3
struct student /* 定义结构 */
{
long num;
char name[10];
int age;
char address[10];
} stu[SIZE],out;
void fsave ( );
main ( )
{
FILE *fp;
int i;
for(i=0;i<SIZE;i++) /* 从键盘读入学生的信息(结构) */
{
printf("Input student %d:", i+1);
scanf("%ld%s%d%s",&stu[i].num,stu[i].name,&stu[i].age,stu[i].address);
}
fsave( ); /* 调用函数保存学生信息 */
fp = fopen ("d:\\student", "rb"); /* 以二进制读方式打开数据文件 */
printf (" No. Name Age Address\n");
while ( fread(&out, sizeof(out),1,fp) ) /*以读数据块方式读入信息 */
printf("%8ld %-10s %4d %-10s\n",out.num,out.name,out.age,out.address);
fclose(fp); /* 关闭文件 */
}
void fsave ( )
{
FILE *fp;
int i;
if((fp=fopen("d:\\student","wb"))==NULL) /* 二进制写方式 */
{
printf ("Cannot open file.\n");
return;
}
for(i=0; i<SIZE; i++ ) /*将结构以数据块形式写入文件 */
if(fwrite(&stu[i],sizeof(struct student),1,fp)!=1)
printf("File write error.\n"); /* 出错处理 */
fclose (fp); /* 关闭文件 */
}
输出结果:
D盘产生一个新的相应文件
5.文件的随机读写
在有些情况下我们并不需要顺序读写文件的全部数据,而是要关注文件中的特定信息。这样就需要对文件读写的位置进行自由的设置
文件读写操作的内部机制
文件的读写是通过文件系统内部的"位置指针"进行的。当打开一个文件时系统自动为打开的文件建立一个位置指针,对该文件的读写操作均通过位置指针进行操作。fopen后位置指针指向文件中第1个(将要读写)字节。文件结束时位置指针指向文件最后1个字节的下1个位置
顺序读写操作
在顺序读写文件时文件的位置指针由系统自动控制,每次读写操作后系统都会将位置指针移到下一个数据的位置。在不改变位置指针的情况下只能对文件进行顺序操作。
随机读写操作
通过改变文件的位置指针可在文件的任意位置进行读写操作。
位置指针移动(fseek)
fseek(fp,offset,positon);
参数:FILE *fp(将要移动的指针所指的文件);long offset(指针移动的偏移量,正数表示向文件尾部也就是正常读取的顺序移动,负数则逆着正常读取的方向移动);int positon(表示移动的基准位置)
函数功能:将指定文件fp的文件位置指针按照position规定的方向移动offset个字节。移动成功返回值为0,否则返回值为非0。position为起始点,指出以文件的什么位置为基准进行移动:
0:文件的开头 1:文件的当前位置 2:文件的末尾
fseek(fp,50,0) //从文件头开始向前(文件尾方向)移动50个字节
fseek(fp,-10,1) //从当前位置向后(文件头方向)移动10个字节
fseek(fp,-20,2) //从文件尾开始向后(文件头方向)移动20个字节
位置指针返回到文件头(rewind)
rewind(fp);
参数:FILE *fp;
函数功能:使fp指定的文件的位置指针重新定位到文件的头位置。
取位置指针(ftell)
ftell(fp);
参数:FILE *fp;
函数功能:得到fp所指向文件的当前读写位置,即位置指针的当前值。该值是个长整型数,是位置指针从文件开始处到当前位置的位移量的字节数。如果函数的返回值为-1L,表示出错
6.文件检测
文件是一个复杂的数据集,对它的操作难免会出现各种状况。因此我们需要一些手段来对文件的状态和函数调用的情况进行反馈
C语言提供了两种手段来反映函数调用的情况和文件的状态。
1.由函数的返回值可以知道文件调用是否成功。
2.在调用完成后由C函数库提供对文件操作状态和操作出错的检测函数:
检测文件结束函数、
检测文件出错函数、
清除出错标记及文件结束标记
检测文件结束函数
feof(fp)
功能:若文件已经结束返回值为非0;若文件尚未结束返回值为0。
注:之前我们所学的两种不同的文件。一种是文本文件,它在结束的时候读取的最后一个字符是EOF。而如果我们读取的是一个二进制文件就不能通过这样的方式来判断结束,必须使用feof这样一个文件结束函数来判断文件是否结束
检测文件出错函数
ferror(fp)
功能:若文件出错返回值为非0;若文件未出错返回值为0。
清除出错标记及文件结束标记
在函数执行的过程中我们通过文件结束以及出错函数可以检测到相关标记,为了避免这种标记对后续操作的影响,需要清除出错标记及文件结束标记
clearerr(fp)
功能:清除文件fp的出错和文件结束标记
7.文件应用实例
我们可以通过文件来保存程序的数据来提高程序的实用性,下为几个应用
例1:
#include<stdio.h>
#include<stdlib.h>
main()
{
FILE *fp;
int i;
char s[20],t[20];
gets(s);
fp=fopen("d:\\stu.txt","w");
for(i=0;s[i]!='\0';i++)
fputc(s[i],fp);
fclose(fp);
fp=fopen("d:\\stu.txt","r");
fgets(t,6,fp);
printf("The first t is :%s\n",t);
printf("The current position is:%d\n",ftell(fp));
fseek(fp,5,1);
printf("The current position is:%d\n",ftell(fp));
fgets(t,3,fp);
printf("The second t is :%s\n",t);
rewind(fp);
printf("The current position is:%d\n",ftell(fp));
fgets(t,3,fp);
printf("The last t is :%s\n",t);
fclose(fp);
}
输出结果:
并在D盘中产生一个stu.txt(之前不需要事先建立一个txt文档)
例2:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
main( )
{
FILE *fp;
char str[81],filename[80];
gets(filename);
if((fp=fopen(filename,"w"))==NULL)
{
printf("can not open this file\n");
exit(0);
}
while(strlen(gets(str))>0 )
{
fputs(str,fp);
fputs("\n",fp);
}
fclose(fp);
}
输出结果:
在D盘中产生的相应文件