一.相关概念
1.文件信息区
每个要被使用的文件(比如要进行读或写操作),内存中都会开辟一个相应的文件信息区,用来存放文件的有关信息(如文件的名字,位置,状态等).这些信息是保存在一个结构体变量中的,该结构体类型是由系统声明的,取名FILE.
知道了FILE该结构体是用来保存文件的相关信息的,那么下面来看看该c编译环境提供的stdio.h头文件中该结构体的定义代码
typedef struct
{
short level; //缓冲区"满"或"空"的程度
unsigned flags; //文件状态标志
char fd; //文件描述符
unsigned char hold; //如缓冲区无内容不读取字符
short bsize; //缓冲区的大小
unsigned char *buffer; //数据缓冲区的位置
unsigned char *curp; //指针当前的指向
unsigned istemp; //临时文件指示器
short token; //用于有效性检查
}FILE;
2.文件类型指针
这样声明: FILE *fp; 就得到了一个文件指针fp,那么这个文件指针是什么呢? 其实就是指向FILE结构体内存空间的指针. 上面第一点也说到,被使用的文件会在内存中开辟一片文件信息的存储空间,里面存储了文件的相关信息,所以FILE *fp=fopen(“flie.text”,“r”); 的意思就是file.txt被使用了,其文件的相关信息被加载进内存中,然后我们声明的文件指针就获得了该片内存的地址,有了地址我们就可以对文件进行相关操作了.就好比我们获得了某个整形变量的地址,我们通过指针就能改变整形变量的值了,一个意思.
3.文件缓冲区
其实数据并不是直接就从内存输出到磁盘或从磁盘输出到内存的,他们之间要经过一个文件缓冲区.内存区会为程序中每个正在使用的文件开辟一个文件缓冲区.从内存向磁盘输出数据必须先送到内存的缓冲区,装满缓冲区后才一起送到磁盘中去.如果从磁盘向计算机读入数据,则一次从磁盘文件将一批数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(给程序变量).
4.fopen(文件名,文件使用方式)
C程序设计(第四版)中的文件使用方式介绍
文件使用方式 | 含义 | 如果指定的文件不存在 |
---|---|---|
“r”(只读) | 为了输入数据,打开一个已存在的文本文件 | 出错 |
“w”(只写) | 为了输出数据,打开一个文本文件 | 建立新文件 |
“a”(追加) | 向文本文件末尾添加数据 | 出错 |
“rb”(只读) | 为了输入数据,打开一个二进制文件 | 出错 |
“wb”(只写) | 为了输出数据,打开一个二进制文件 | 建立新文件 |
“ab”(追加) | 向二进制文件末尾添加数据 | 出错 |
“r+”(读写) | 为了读和写,打开一个文本文件 | 出错 |
“w+”(读写) | 为了读和写,打开一个文本文件 | 建立新文件 |
“a+”(读写) | 为了读和写,打开一个文本文件 | 出错 |
“rb+”(读写) | 为了读和写,打开一个二进制文件 | 出错 |
“wb+”(读写) | 为了读和写,打开一个二进制文件 | 建立新文件 |
“ab+”(读写) | 为了读和写,打开一个二进制文件 | 出错 |
在DEV-C++中的实际操作情况
文件使用方式 | 含义 | 如果指定的文件不存在 |
---|---|---|
“r”(只读) | 打开一个文件,只能读 | 出错 |
“w”(只写) | 打开一个文件,清空原文件的所有内容,只能写 | 建立新文件 |
“a”(追加) | 打开一个文件,向文件末尾添加数据 ,只能写 | 建立新文件 |
“rb”(只读) | 打开一个文件, 只能读 | 出错 |
“wb”(只写) | 打开一个文件,清空原文件的所有内容,只能写 | 建立新文件 |
“ab”(追加) | 打开一个文件,向文件末尾添加数据 ,只能写 | 建立新文件 |
“r+”(读写) | 打开一个文件,可读写 | 出错 |
“w+”(读写) | 打开一个文件,清空原文件的所有内容,可读写 | 建立新文件 |
“a+”(读写) | 打开一个文件,向文件末尾添加数据 ,可读写 | 建立新文件 |
“rb+”(读写) | 打开一个文件,可读写 | 出错 |
“wb+”(读写) | 打开一个文件,清空原文件的所有内容,可读写 | 建立新文件 |
“ab+”(读写) | 打开一个文件,向文件末尾添加数据 ,可读写 | 建立新文件 |
温馨提示:
1.通过上面两个表,需要注意一定特别重要的一点,就是"w" , “wb” , “w+” , “wb+” 这四种方式会清空原文件的所有内容,所以这是四个方式要慎用,建议仅在新建文件或要特意清空文件内容时使用.
2.通过实际操作我发现"a" , “ab” , “a+” , “ab+” 这四个操作在指定文件不存在时也能够新建文件,当然这个可能是因为编译器或者c语言版本的原因,既然书上说了不能新建,建议就按标准来吧,用"w" , “wb” , “w+” , "wb+"这四个操作来新建文件吧.
3.使用"a+"或者"ab+"进行读操作时,要记得将文件读写位置置于有内容的位置(下文会介绍相关函数),因为打开方式为"a+"或者"ab+"的指针初始时文件读写位置在文件末尾.
5.数据文件的类型
根据数据的组织形式,数据可分为ASCII文件和二进制文件.数据在内存中是以二进制形式存储的.而磁盘中可以存储二进制类型或ASCII类型.
二.文本文件(ASCII形式)读写函数介绍
1. 非格式化读写文件
1.fopen(文件指针,文件操作方式) //打开文件失败返回NULL
功能:打开文件,使其文件信息载入内存,然后返回指定文件的信息区的地址,若打开文件失败返回NULL
例子: FILE *fp=fopen(“file.txt”,“r”);//fp获得文件相关信息的存储区的地址,可通过指针fp进行相关操作
2.fclose(文件指针)
功能:关闭文件,撤销文件信息区和文件缓冲区,使文件不能在被操作,除非再次载入(fopen),成功关闭返回0,失败返回EOF(-1)
例子:fclose(fp); //关闭文件,撤销文件信息区和文件缓冲区,fp不再指向文件信息区,不能再通过该指针进行相关操作
3.fgets(str,n,fp) //参数分别是(字符数组,读取字符数量,文件指针)
功能:从fp指向的文件读入一个长度为(n-1)个字符的字符串,然后末尾加上一个字符串结束字符’\0’并存放到字符数组str中,读成功返回地址str,失败则返回NULL
4.fputs(str,fp) //参数分别是(字符数组,读取字符数量,文件指针)
功能:把字符数组str中的字符串写入到文件指针fp所指向的文件中.写成功返回0,失败返回非0值
5.fgetc(fp)
功能:从fp指向的文件读入一个字符,读成功返回所读字符,失败返回文件结束标志EOF(-1)
例子: char ch=fgetc(fp); //文件中读入一个字符并赋值给变量ch
6.fputc(ch,fp)
功能:把字符ch写到文件指针变量fp所指向的文件中,写入成功返回值就是写入的字符,失败返回EOF(-1)
注意:
1.就算你用的是二进制的文件操作方式,fputs函数也只能向文件中写入ASCII类型文件内容
2.就是你用的是二进制的文件操作方式,fgets函数也能从ASCII类型文件中正确读取内容.
接下来实验一下验证一下
#include"stdio.h"
int main()
{
FILE *fp=fopen("信息表","wb"); //二进制写入方式
char str[20]={"apple"};
fputs(str,fp); //写数据
fclose(fp);
FILE *fp2=fopen("信息表","rb"); //二进制读入方式
char str2[20];
fgets(str2,6,fp2); //读数据,会读入n-1个字符,apple有5个字符,所以填6
fclose(fp2);
printf("信息表内容:%s\n",str2);
return 0;
}
操作结果:
控制台输出显示
文件内容
可以看到验证结果正确
好了,继续函数介绍
2.格式化读写文件(不推荐使用)
1.fprintf(文件指针,格式字符串,输出列表);
功能:使用格式化向文件中写入内容(ASCII码类型数据),类似于printf
例子:fprintf(fp,"%s",ch); //把字符串ch的内容写入文件中
2.fscanf(文件指针,格式字符串,输入列表);
功能:使用格式化从文件中读入内容(ASCII码类型数据),类似于scanf
例子:fscanf(fp,"%s",ch); //把文件中的一串字符串读入并赋值给字符串数组ch
操作示例:
#include"stdio.h"
typedef struct
{
char name[20]; //姓名
int old; //年龄
float height; //身高
}stu;
int main()
{
FILE *fp=fopen("信息表\0","wb"); //二进制写入方式
stu S1={"Andy",20,180.5};
fprintf(fp,"%s,%d,%f",S1.name,S1.old,S1.height); //写数据
fclose(fp);
FILE *fp2=fopen("信息表","rb"); //二进制读入方式
stu S2;
fscanf(fp2,"%s,%d,%f",S2.name,&S2.old,&S2.height); //读数据
fclose(fp2);
printf("姓名:%s\n年龄:%d\n身高:%f",S2.name,S2.old,S2.height);
return 0;
}
操作结果
这是怎么回事?原来字符数组name容量太大了,一次性把所有内容都读进数组了,所以年龄和身高都不能被读取到了…
注意事项:
1.读文件时,fscanf(文件指针,格式字符串,输入列表);函数是按格式字符串的顺序来读取文件的,所以格式字符串顺序要与文件内容中类型顺序一样,如果文件中是:1.123456,12 那么格式字符串的顺序应该是%f,%d 否则读入的字符会有错误,系统是不会自动识别变量类型而自动把匹配类型的字符赋给它们的,所以文件内容中的类型顺序要和格式字符串顺序保持一致
2.写文件时,fprintf(文件指针,格式字符串,输入列表);格式字符串类型要记得加上逗号隔开,如%d,%d 切记不要%d%d,因为比如写入20和180,那么文件中被写入内容为20180,可是这样是表示几个数呢? 那么fscanf(fp,"%d,%d",a,b)根本无法分别读入20和180,一次性就读入了整形20180并赋值给a,所以会导致从文件中读取内容时出错,因此切记格式字符串类型要记得加上逗号或空格隔开,如%d,%d,这样写入的内容就是20,180 ,之间有逗号隔开,表示有两项数据
列举两种正确方式
//第一种,用逗号隔开
fprintf(fp,"%d,%d",a,b); //写
fscanf(fp,"%d,%d",a,b); //读
//第二种,用空格隔开
fprintf(fp,"%d %d",a,b); //写
fscanf(fp,"%d %d",a,b); //读
//注意:fscanf要于fprintf格式字符串类型的隔开方式一致,
// 即fprintf格式字符串类型用逗号隔开,那么fscanf也要用逗号隔开
3.用fscanf或fgets,读文件时,要将文件中的ASCII码转换为二进制形式再保存在内存变量中,用fprintf写文件时又要将内存中的二进制形式转换为ASCII码,然后才写入文件中,这样要花费很多时间,所以在大量文件读写操作时不推荐使用fgets,fputs,fprintf,pscanf.
三.二进制文件读写函数介绍(推荐使用)
1.前言:
前面介绍了fgets,fputs函数,但这两种函数只能适用于每次读入或写入都是相同字符数量的数据项的情况,要是数据字符数不相同就会造成数据混乱.而fscanf ,fprintf不太适合字符串类型的数据,fscanf对于相邻数据项是字符串类型会无法识别有几项数据项,造成读的错误.
而且以上这些方式都是先将内存中的数据由二进制转换为ASCII形式,以ASCII码的文件内容形式写入磁盘的,而读入时,又要将磁盘中文件内容的ASCII码转化为二进制才能读入内存.这样相互转换会耗费很多时间,所以不推荐使用,于是下面推荐二进制的文件读写方式
2.函数介绍
1.fwrite(buff,size,count,fp)//函数执行成功会返回1,失败返回0
功能:文件内容的读写位置后移size个字节,每移动一个字节就写入一个字节的数据,这样就将buff指向的变量中的size个字节数据内容写入了磁盘的文件中,执行count次,也就是写入count项数据
2.fread(buff,size,count,fp) //函数执行成功会返回1,失败返回0
功能:文件内容的读写位置后移size个字节,每移动一个字节就读入一个字节的数据,执行count次,也就是读入count项数据,然后把从文件中读入的size*count个字节的数据内容并保存buff指向的变量内存空间中
说明:
buff: 变量的地址,fread中是读入文件中的数据并保存在变量中,buff就是那个变量的地址, fwrite中是将变量中的数据写入到文件中,buff就是那个变量的地址
size: 要读写的字节数
count: 要读写多少个数据项(每个数据项长度为size)
fp: FILE类型指针
3.rewind(文件指针)
功能:将文件读写位置置为文件内容第一个字节(文件内容从第1个字节开始读写)的前一字节位置,也就是0字节位置(文件中实际上并没有这个位置,只是个假定位置,为了方便操作而已)
4.fseek(文件指针,位移量,起始点)
起始点 | 名字 | 用数字代表 |
---|---|---|
文件内容第一个字节的前端(0字节的位置) | SEEK_SET | 0 |
文件内容当前读写位置 | SEEK_CUR | 1 |
文件内容的最后一个字节处 | SEEK_END | 2 |
"位移量"指以"起始点"为基点,向前移动的字节数.位移量应是long型数据(在数字的末尾加一个字母L,就是表示long型).
fseek函数一般用于二进制文件.下面是fseek函数调用的几个例子
fseek(fp,100L,0);//将文件读写位置移动到离文件内容第1个字节的前端位置(0字节位置)的第100字节位置,也就是0+100
fseek(fp,100L,1);//将文件读写位置移动到离当前位置100字节的位置,也就是当前位置+100
fseek(fp,-100L,2);//将文件读写位置移动到离文件内容的最后一个字节的位置-100字节的位置,也就是文件内容最后一个字节位置-100
5.ftell(文件指针)
功能:返回文件读写的当前位置
6.feof(文件指针)
功能:检测是否读到文件的结束标志(末尾无字节内容处为结束标志),如果是返回true,否返回false
注意:这个函数并不推荐用来判断是否读到文件内容的末尾,因为假如文件内容有5项数据,
每项数据4个字节, while(!feof(fp)) 作为循环,当我们用5次循环读入文件内容后,文件读入位置是在第20字节处,这时feof(fp)判断第20个字节处没有读到结束标志,是有内容的,则返回false,所以还会进行一次循环,也就是进行第6次循环,这也就是为什么会多输出一项.
接下来我们操作比对一下
先用feof(fp)函数来作为读入结束判断条件
#include"stdio.h"
#include"string.h"
typedef struct
{
char name[20]; //姓名
int number; //学号
}stu; //占24个字节
int main()
{
FILE *fp=fopen("信息表","wb"); //二进制写入方式
printf("\n姓名:");
stu S;
scanf("%s",S.name);
while(strcmp(S.name,"#")!=0) //以输入'#'结束输入操作
{
printf("学号:");
scanf("%d",&S.number);
fwrite(&S,sizeof(stu),1,fp); //向文件中写入一项结构体中数据,24个字节
printf("\n姓名:");
scanf("%s",S.name);
}
fclose(fp);
FILE *fp2=fopen("信息表","rb");
int pos=ftell(fp2); //读写位置
printf("\n初始读写位置=%d\n\n",pos);
stu S2;
while(!feof(fp2))
{
pos=ftell(fp2);
printf("读操作之前的读写位置=%d\n",pos); //显示读操作之前的读写位置
fread(&S2,sizeof(stu),1,fp2); //读入一项数据,一项24个字节
printf("姓名:%s 学号:%d\n",S2.name,S2.number);
pos=ftell(fp2);
printf("读操作之后的读写位置=%d\n\n\n",pos); //显示读操作之后的读写位置
}
fclose(fp2);
return 0;
}
操作结果
是吧,多进行了一次循环.
是这样的,当读完内容的最后一项(第3项)数据时,此时文件读写位置位于内容的最后一个字节处,也就是72字节处,而用feof(fp)判断72字节处是否读到结束标志,72字节处是有内容的,所以返回fasle,所以会进行第4次循环.
第四次循环过程是这样的:执行fread函数,读写位置先从72字节位置后移一个字节然后读入一个字节的数据,但是第73个字节的位置并没有内容,所以读到文件的结束标志,故读入数据失败,然后读写位置会从73自动回到72,但是为什么还会显示内容?那是因为读入数据失败,结构体S2没有被重新赋值,显示的是上一次循环的值,然后再回到循环条件判断,因为刚读73字节位置时读到了结束字符,feof(fp)此时就为真了,所以循环退出.
所以这里建议使用fread函数的返回值作为读入数据的结束标志
操作一下
#include"stdio.h"
#include"string.h"
typedef struct
{
char name[20]; //姓名
int number; //学号
}stu; //占24个字节
int main()
{
FILE *fp=fopen("信息表","wb"); //二进制写入方式
printf("\n姓名:");
stu S;
scanf("%s",S.name);
while(strcmp(S.name,"#")!=0) //以输入'#'结束输入操作
{
printf("学号:");
scanf("%d",&S.number);
fwrite(&S,sizeof(stu),1,fp); //向文件中写入一项结构体中数据,24个字节
printf("\n姓名:");
scanf("%s",S.name);
}
fclose(fp);
FILE *fp2=fopen("信息表","rb");
int pos=ftell(fp2); //读写位置
printf("\n初始读写位置=%d\n\n",pos);
stu S2;
while(fread(&S2,sizeof(stu),1,fp2))
{
printf("姓名:%s 学号:%d\n",S2.name,S2.number);
}
pos=ftell(fp2); //读写位置
printf("\n读数据操作完成后读写位置=%d\n\n",pos);
fclose(fp2);
return 0;
}
操作结果
是吧,这样就没什么毛病
上面是两个指针进行的操作(一个用来写,一个用来读),当然我们也可以只用一个指针来操作(读写都是同一个指针),即使用可读可写的打开方式,但是注意要使用rewind函数或fseek函数,把文件读写位置移动到有内容的位置
操作一下
#include"stdio.h"
#include"string.h"
typedef struct
{
char name[20]; //姓名
int number; //学号
}stu; //占24个字节
int main()
{
FILE *fp=fopen("信息表","wb+"); //二进制写入方式
printf("\n姓名:");
stu S;
scanf("%s",S.name);
while(strcmp(S.name,"#")!=0) //以输入'#'结束输入操作
{
printf("学号:");
scanf("%d",&S.number);
fwrite(&S,sizeof(stu),1,fp); //向文件中写入一项结构体中数据,24个字节
printf("\n姓名:");
scanf("%s",S.name);
}
rewind(fp); //将文件读写位置至于开头位置(0位置),使用fseek(fp,0L,0);也是等效的
int pos=ftell(fp); //读写位置
printf("\n初始读写位置=%d\n\n",pos);
stu S2;
while(fread(&S2,sizeof(stu),1,fp))
{
printf("姓名:%s 学号:%d\n",S2.name,S2.number);
}
pos=ftell(fp); //读写位置
printf("\n读数据操作完成后读写位置=%d\n\n",pos);
fclose(fp);
return 0;
}
操作结果
温馨提示:
1.就算使用的是非二进制的打开方式(w,a等),用fwrite函数写入文件的也是二进制形式的数据,非二进制的打开方式(r,r+等),也照样可以用fread函数来读取二进制文件.当然我们最好还是按照标准来,使用rb,wb,ab等二进制打开方式
2.不能存在2个以上的指针对同一文件同时进行操作,会导致文件数据损坏.即一个指针未关闭时(flcose(fp1)),不能再创建另一个指针(fp2)指向同一文件然后对其进行相关操作.一定要先关闭fp1,才能创建fp2对文件进行操作.
四.文件读写的出错检测
1.ferror函数
在调用各种读写函数(如fgets,fwrite等)时,如果出现错误,除了函数返回值有所反映外,还可以用ferror函数检查.
它的一般调用形式为ferror(fp);如果ferror返回值为0(false),表示未出错;如果返回一个非零值,表示出错.应该注意,对同一个文件每一次调用读写函数,都会产生一个新的ferror函数值,所以应当在调用一个读写函数后立即检查ferror函数的值,否则信息会丢失.
在执行fopen函数时,ferror函数的初始值自动置为0.
2.clearerr函数
clearerr的作用是使文件错误标志和文件结束标志置为0. 假设在调用一个读写函数出现错误,ferror函数值为非零值.应该立即调用clearerr(fp),使ferror(fp)的值变为0,以便在进行下一次的检测.