前言在谭浩强的C语言设计的第10张讲了有关文件的知识,
以往我们写的C程序的数据都是由键盘输入的
现在我们要对文件进行操作。
文件是操作系统的基本单位。是我们专业领域的重要的一部分。
就拿我们的编译器来说,我们写了一个程序,成功的运行了,编译系统会主动的生成3个文件如下图
它们分别是源程序文件.c
目标文件.o
可执行文件.exe
在实际的情况下,常常要把一些数据输出到磁盘上保存起来,在需要时送入内存之中这就需要我们用到磁盘文件。
1.文件名
一个文件要有一个唯一的文件标识以便识别和引用,分别为
1.路径(用来找到文件)
2.名称(识别是什么)
3.后缀(文件的格式或属性)
2.文件的分类
书上将文件分成两类,分别是
ASCII文件和二进制文件
1.ASCII文件我们在刚刚接触c语言时就了解了ASCII标,每个特定的数代表一个字符,那么将字符形式的文件就是ASCII文件,也称为文本文件,每个字节存放一个字符的ASCII值。
2.二进制文件
数据在内存中是以二进制形式存储的(因为CPU只认识0和1),如果不加转换的输出到外存,就是二进制文件。也称之为映像文件
(因为从二进制转换到ASCII需要一定的时间,占的空间也更多,加之二进制文件的实用优点,我们生活中常用二进制的文件。)
3.缓冲区概念
我们都知道,在计算机的存储设备中,CPU的缓存速度是最快的,其次是显卡的显存,DDR5的显存早就已经普及,并有被淘汰之势,再次是内存,就在这几年DDR4的内存才开始广泛的使用,最慢的就是我们的硬盘(磁盘),机械硬盘的转速很慢,最快的速度也只有200多MB一秒,尽管我们现在普遍用上了固态硬盘,还是M.2协议的,但是和内存的差距依旧很大。因为两者的差距太大,就必须在数据写入磁盘时就有必要在内存中开辟一片缓冲区(所以缓冲区不是什么高大上的东西)
4.文件类型指针
回顾前面的知识,指针必须要有基类型(指向),如int p,floatp…。
那么顾名思义文件指针就是指向文件的,其实文件在C语言中是,一种结构体
也就是和结构体指针是一样的。
以下是文件结构体的定义,结构体的名称为FILE。
tpyedef struct
{ short level; //缓冲区满或空的程度
unsigned flags; //文件状态标志
char fd; //文件描述符
unsigned char hold; //如缓冲区无内容不读取字符
short bsize; //缓冲区的大小
unsigned char*buffer; //数据缓冲区的位置
unsigned char*curp; //文件位置标记指针当前的指向
unsigned istemp; //临时文件指示器
short token; //用于有效性检查
}FILE
//这是TC2.0中的定义
#ifndef _FILE_DEFINED
struct _iobuf {
char *_ptr; //文件输入的下一个位置
int _cnt; //当前缓冲区的相对位置
char *_base; //指基础位置(即是文件的其始位置)
int _flag; //文件标志
int _file; //文件的有效性验证
int _charbuf; //检查缓冲区状况,如果无缓冲区则不读取
int _bufsiz; //缓冲区大小
char *_tmpfname; //临时文件名
};
typedef struct _iobuf FILE;
#define _FILE_DEFINED
#endif //这是VC6。0里定义
当然我们不必去深究其中的作用(会写标准库定义的人不知道比我们高到哪里去了)
但要稍微了解一下
下面介绍关于文件的函数
1.打开与关闭文件函数
fopen 打开文件函数 (成功打开后指向该流的文件指针就会被返回,失败返回NULL)
fclose 关闭文件函数
引用方法
fopen(文件名,使用文件方式); //文件名是一个字符串,使用方式需要加双引号,
为了使文件指针与文件建立联系我们要将函数返回的指针给我们的文件指针
像这样
FILE*fp;
fp = fopen("test.txt","r");//以读的方式打开默认路径下一个叫test的文本文件
可以说这样就将fp指向了test这个文件。
既然文件的使用方式,我们就要全面的了解
文件使用方式 | 含义 | 如果指定的文件不存在 |
---|---|---|
r(只读) | 输入数据,打开一个已存在文本文件 | 出错 |
w(只写) | 输出数据,打开一个文本文件 | 建立一个新文件 |
a(追加) | 向文本文件尾添加数据 | 出错 |
rb(只读) | 输入数据,打开一个2进制文件 | 出错 |
wb(只写) | 输出数据,打开一个2进制文件 | 建立新文件 |
r+(读写) | 读写,文本文件 | 出错 |
w+(读写) | 读写,文本文件 | 建立新文件 |
a+(读写) | 读写,文本文件 | 出错 |
rb+(读写) | 读写,二进制文件 | 出错 |
wb+(读写) | 读写,二进制文件 | 建立新文件 |
ab+(读写) | 读写二进制文件 | 出错 |
这里我们来用一个小程序演示一下
我现在编译器默认路径下建立一个空的文本文件test
#include
int main()
{
FILE*p; //设置一个文件指针
char ch; //定义一个字符变量
if((p=fopen("test.txt","w"))==NULL) //打开一个叫test的文本文件,以只写的方式,当 出错时,即返回值为NULL时,输出错误信息
{
printf("ERROR");
exit(0); //关闭所有文件,终止正在执行的程序
}
for(;ch!='\n';) //输入ch知道输入空格。
{
scanf("%c",&ch);
fputc(ch,p); //将字符ch写入p所指向的文件
}
fclose(p);//关闭文件
}
可以看到我们在键盘里输出的Merry Christmas已经被写入了test之中
下面我们来读取这个文件里的信息
我们只需要将程序稍微修改一下就可以
#include
int main()
{
FILE*p;
char ch;
if((p=fopen("test.txt","r"))==NULL) //以只读的方式打开test。
{
printf("ERROR");
exit(0);
}
for(;ch!='\n';)
{
ch=fgetc(p); //ch得到p所指文件中的每一个字符
putchar(ch); //将得到的字符输出到屏幕
}
fclose(p); //关闭文件
}
值得注意的是,在我们向文件写入的时候会将原有的内容清空再进行写入
请看我们再对test进行写入
这次我输入了happy
再到默认路径下打开text文件
可以看到我们的Merry Christmas被清除了并用我们的happy写入了文件
如果要对文本文件 进行写入并保留原有内容,就要用追加的方式进行
if((p=fopen("test.txt","a"))==NULL) //只需将w改为a即可
5.顺序读写数据文件
1.读写字符函数
读字符 fgetc
fegrtc(文件指针) //从指针所指向的文件中读入一个字符
写字符fputc
fputc(ch,fp) //把字符变量ch写到fp所指向的文件中去
我们在上一个例子中就使用了这俩个函数,为了有更深刻的印象。
我以一个文件复制的程序来中举例
下面是源码
#include
int main()
{
FILE*p1,*p2; //设置2个文件指针
char filename[30],filename1[30],ch; //设置2个字符数组用来输入文件名用
printf("请输入要复制的文件名\n");
gets(filename); //输入文件名
printf("请输入复制后的文件名:\n");
gets(filename1); //输入文件名
if((p1=fopen(filename,"rb"))==NULL) //打开被复制的文件
{
printf("ERROR");
exit(0);
}
if((p2=fopen(filename1,"wb"))==NULL) //写入要复制的文件名
{
printf("ERROR");
exit(0);
}
while(!feof(p1)) //用一个检查文件是否结束的函数来判断
{
ch=fgetc(p1); //读出每一个p1指向的文件中的字节,把ch写入到p2指向的文件中去,如果没有p2文件,则会建立一个以filename1字符数组命名的文件
fputc(ch,p2);
}
printf("复制成功");
fclose(p1); //用完之后,为了避免不必要的操作干扰读写,要关闭文件,即断掉文件指针与文件的联系
fclose(p2);
}
下面是运行效果,我的默认路径中有一张名为23.jpg的图片文件
运行程序
写入如下
再去查看默认路径
就多了一张叫jaychou的jpg图片
下面就要介绍检测文件是否介绍的函数
feof(end of file)
feof(文件指针) //当文件结束时返回非0值,当文件未结束时返回0
再看刚才的循环语句
while(!feof(fp))//当返回值为0的时候执行循环,返回值非0的时候就结束循环
当然对于文本文件文件可以不用二进制的方式处理
我们也可以把刚才的循环语句换成如下
while(!(ch=fgetc(fp))==EOF) //我们的EOF和NULL一样都是标准库里的宏定义EOF就是-1,NULL代表0
那么这个-1又代表了什么
文件的所有有效字符后有一个文件尾标志,当读完全部的字符后,文件读写位置标记就会指向最后一个字符的后面的结束字节(里面存放了-1),如果再读取就会读出-1
在文本文件中,数据都是以字符的ASCII代码值的形式存放。我们知道,ASCII代码值的范围是0~127,不可能出现-1,因此可以用EOF作为文件结束标志。
WARNING二进制文件因为是以二进制形式保存的所以不能以字符的方式来存取的,所以不应用刚才的方式对二进制文件进行写入。
同样
我们也有字符串的读写文件
函数名 | 调用形式 | 功能 | 返回值 |
---|---|---|---|
fgets | fgets(str,n,fp) | 从fp指向的文件读入一个长度为n-1的字符串,存放到字符数组str中去 | 成功返回地址str,失败返回NULL |
fputs | fputs(str,fp)) | 把str所指向的字符串写入fp所指向的文件中 | 成功返回0,失败返回非零 |
下面来举个例子
现在test中写如下内容
我们用fgets函数读取出来并输出上面的文字
#include
#define LEN 40
int main()
{
FILE*fp;
char filename[LEN],string[30]; //定义一个字符数组来存储文件里的信息
printf("请输入要打开的文件名");
gets(filename);
if((fp=fopen(filename,"r"))==NULL) //以只读的方式打开文件打开文件
{
printf("ERROR");
exit(0);
}
fgets(string,20,fp); //将fp所指的文件中的20字符读取给字符串string
fclose(fp); //关闭文件
puts(string); //以字符串形式输出string
}
6.用格式化的方式读写文本文件
在我们以前的输出输入之中,常用scanf和printf函数,我们的格式化读写函数和他们类似
名称 | 引用方式 |
---|---|
fprintf | fprintf(文件指针,格式字符串,输出列表) |
fscanf | fscanf(文件指针,格式字符串,输入列表) |
#include
int main()
{
int i=3;float j=4.567;char string[20];
FILE*fp;
if((fp=fopen("test.txt","r+"))==NULL) //以读写的方式打开test
{
printf("ERROR");
exit(0);
}
fscanf(fp,"%s",string); 把其中的字符串写入字符数组string中
puts(string); //输出由文件中写入的字符串
fprintf(fp,"%3d,%6.4f",i,j); //以%3d,%6.4的格式输入整形变量i和浮点型变量j
fclose(fp); //关闭文件
}
再去默认路径中打开test文件
7.用二进制方式向文件读写一组数据
其实我们上面的复制图片的例子也是对二进制文件的读写,在这里有一组专门的二进制文件读写函数
分别是
fread
fwrite
名称 | 引用方式 |
---|---|
fread | fread(buffer,size,count,fp) |
fwrite | fwrite(buffer,size,count,fp) |
(也被称为数据块读写)
函数原型size_t fread ( void *buffer, size_t size, size_t count, FILE *stream)
参 数
buffer
用于接收数据的内存地址(是一个指针,无论输出输入都是首地址)
size
要读的每个数据项的字节数,单位是字节
count
要读count个数据项,每个数据项size个字节.
stream
文件指针
数据块读写多用于结构体变量的读写(因为结构体所占的字符数是不规则的)
下面举一个例子
#include
#define LEN 15 //结构体中字符串的长度
#define NUM 8 //要输入数据学生的个数
struct Student //定义学生结构体
{
char name[LEN];
int num;
int age;
char add[LEN];
}Stud[NUM]; //将数据先存放在结构体数组之中
int main()
{
FILE*fp;
int i;
if((fp=fopen("stduent","wb"))==NULL) //建立一个叫student的文件以二进制形式进行打开进行写入操作,
{
printf("ERROR");
exit(0);
}
printf("请输入学生数据\n");
for(i=0;i<NUM;i++) //
{
scanf("%s%d%d%s",&Stud[i].name,&Stud[i].num,&Stud[i].age,&Stud[i].add);//输入学生数据信息
}
for(i=0;i<NUM;i++)
{
if(fwrite(&Stud[i],sizeof(struct Student),1,fp)!=1)//每次写一个结构体变量所占的字节,将输入的数据写入文件
{
printf("write error");
}
}
fclose(fp); //关闭文件
}
完成输入如图
当我们去默认路径找到student文件,发现并不是文本文件,用文本文本的格式打开会看见二进制的源码转化出的乱码
我们再改变一下以上的代码,将student的文件中的学生数据读取出来
按照上面的输入稍微修改一下即可
下面是代码
#include
#define LEN 15
#define NUM 8
struct Student
{
char name[LEN];
int num;
int age;
char add[LEN];
}Stud[NUM];
int main()
{
FILE*fp;
int i;
if((fp=fopen("student","rb"))==NULL) //以二进制的方式读取
{
printf("ERROR");
exit(0);
}
printf("姓名 学号 年龄 地址\n");
for(i=0;i<NUM;i++) //将数据每次一个结构体变量所占字节的个数的数据赋给结构体变量
{
if(fread(&Stud[i],sizeof(struct Student),1,fp)!=1)
{
printf("read error");
}
}
for(i=0;i<NUM;i++) //输出结构体变量的各个成员,输出数据
{
printf("%-10s%4d%4d%-15s",Stud[i].name,Stud[i].num,Stud[i].age,Stud[i].add);
printf("\n");
}
fclose(fp); //关闭文件
}
PS(当文件不在编译器默认的路径时,要在文件名初加上文件的路径,
若一个文件为D:\data\student,我们要输入它的路径如下
D:\data\student因为只是一个反斜杠的话会被和后面的字符被认为是转义字符处理,
这也是我们的老师在我们刚学C语言时就教过的)
8.随机读写数据文件
1.文件位置标记即其定位
文件位置标记其实是一个文件中的一个指针
在我们的顺序读写当中,都是一个字符一个字符的读出有写入
没有出现跳跃的情况,
但在实际使用的过程中,我们不许要每次都要把文件全部都读一遍
只需要稍微的改动某些数据,
下面由本人这个灵魂画师来画一个图演示一下
文件位置标记的定位
rewind 功能是将文件内部的指针重新指向一个文件的开头
rewind(文件指针);
现在把上面的代码改一下
#include
#define LEN 15
#define NUM 8
struct Student
{
char name[LEN];
int num;
int age;
char add[LEN];
}Stud[NUM];
int main()
{
FILE*fp,*fp1;
int i;
if((fp=fopen("student","rb"))==NULL)
{
printf("ERROR");
exit(0);
}
if((fp1=fopen("CSDN","wb"))==NULL) //以写入的方式打开一个文件
{
printf("ERROR");
exit(0);
}
printf("姓名 学号 年龄 地址\n");
for(i=0;i<NUM;i++) //把文件中的数据读入结构体数组的元素
{
if(fread(&Stud[i],sizeof(struct Student),1,fp)!=1)
{
printf("read error");
}
}
rewind(fp); //重置文件指针
for(i=0;i<NUM;i++) //写入新的文件中
{
if(fwrite(&Stud[i],sizeof(struct Student),1,fp1)!=1)
{
printf("read error");
}
}
for(i=0;i<NUM;i++) //输出文件数据
{
printf("%-10s%4d%4d%15s",Stud[i].name,Stud[i].num,Stud[i].age,Stud[i].add);
printf("\n");
}
fclose(fp);
fclose(fp1);
}
运行如前面的图一样,默认路径中出现了一个名为CSDN的文件,里面存放着学生的数据
fseek函数,文件指针位置函数
引用方法为
fseek(文件指针,位移量,起始点);
起始点有3种情况,分别是0,1,2
如表
起始点 | 名字 | 用数字代表 |
---|---|---|
文件开始位置 | SEEK_SET | 0 |
文件当前位置 | SEEK_CUR | 1 |
文件末尾位置 | SEEK_END | 2 |
位移量是long形数据要在结尾加上L
如
fseek(fp,100L,0);//将文件位置标记向前移动到离文件开头100个字节处
fseek(fp,50L,1);//将文件位置标记前移到离当前位置50个字节处
fseek(fp,-10L,2);//将文件位置标记从文件末尾后退10个字节
在上面的代码出修改一下
for(i=0;i<NUM;i+=2)
{
fseek(fp,i*sizeof(struct Student),0);//跳过每次移动2个结构体所占的字节
printf("%-10s%4d%4d%15s",Stud[i].name,Stud[i].num,Stud[i].age,Stud[i].add);
printf("\n");
}
我们就实现了只输出部分的数据
ferror函数
在调用各种输入输出函数(如 putc.getc.fread.fwrite等)时,如果出现错误,除了函数返回值有所反映外,还可以用ferror函数检查。 它的一般调用形式为 ferror(fp);如果ferror返回值为0(假),表示未出错。如果返回一个非零值,表示出错。应该注意,对同一个文件 每一次调用输入输出函数,均产生一个新的ferror函 数值,因此,应当在调用一个输入输出函数后立即检 查ferror函数的值,否则信息会丢失。在执行fopen函数时,ferror函数的初始值自动置为0。
clearerr
clearerr的作用是使文件错误标志和文件结束标志置为0.假设在调用一个输入输出函数时出现了错误,ferror函数值为一个非零值。在调用clearerr(fp)后,ferror(fp)的值变为0。
只要出现错误标志,就一直保留,直到对同一文件调用clearerr函数或rewind函数,或任何一个输入输出函数。
感想:这是我的C语言博客的结束篇,在这2个多月的学习中,有困难也有喜悦,从一个小白,渐渐打开编程的一角,C语言交给我的思想,会帮我开起我的程序人生。
farewell C