C语言文件

上一篇文章:枚举类型

文件概述

在前面的文章中,所有的数据都是通过变量存储在内存单元当中的,这些数据都由程序来处理,一旦程序运行结束,所有的变量将不复存在。前面的文章中所有的输入输出,都是将数据从内存送到标准输出设备(显示器),或从标准输入设备(键盘)送到内存当中。在实际编程中这些操作远远不够。当操作大量数据,或者要与其他程序共享数据,再或者要把一些数据长久保存下来时,就需要文件的读写操作。在C语言中,文件的打开、读写操作都是由库函数来实现的。

什么是文件
文件是指存储在外存储器(硬盘、软盘、光盘、U盘等)上的信息集合。在操作系统中,文件是指驻留在外部介质(磁盘等)中的一个有序数据集, 可以是源文件、目标程序文件、可执行程序、待输入的原始数据或一组输出的结果。文件系统功能是操作系统的重要功能和组成部分。每个文件都有文件名,并且有自己的属性。操作系统对文件的访问都是通过文件名来实现的。文件名的命名规则随操作系统的不同而不同。

文件分类
文件大致可以分为两类,程序文件和数据文件。Windows中的可执行文件(.exe)就是程序文件。数据文件又有文本文件、图像文件、声音文件等。在Windows的记事本中,写好的文本需要保存,才能将其存储到外存储器上,得到长久保存。Windows的记事本是一个可执行的程序文件,我们写好的文本只是存储在内存当中的,保存后,数据才会被写入到磁盘文件中。

文本文件和二进制文件
在C语言中,按数据存数的编码形式,数据文件可分为文本文件和二进制文件。在计算机当中有多种编码方式(ASCII、Unicode、GBK),如果打开一个文件看到了很多奇怪的符号,很有可能是因为乱码的缘故。文本文件是以字符值进行存储与编码的文件,其文件内容就是字符。二进制文件是存储二进制数据的文件。C语言把文件看作数据流(stream),即把文件看成一个字符(字节)序列,文件是由一连串的字节组成,将数据按顺序以一维方式组织存储,对文件的访问以字节为单位。一般来说,二进制文件比文本文件节省存储空间。文本文件在输入时,首先要将字符的ASCII(或Unicode等其他编码形式)码值转换为二进制形式,再送入内存。而二进制文件在读写时不需要转换,所以文本文件比二进制文件的读写速度慢一些。

缓冲文件系统与非缓冲文件系统
CPU的计算速度远远高于外部磁盘,为了提高CPU操作数据的效率,在CPU和磁盘之间还有一个内存缓冲区,即程序和磁盘之间还有一个内存缓冲区,程序只需要和内存缓冲区之间进行交互即可。内存缓冲区和磁盘之间的交互由操作系统来完成。缓冲技术就是在内存中为每一个需要进行读写的文件开辟一块缓冲区,利用缓冲区完成文件操作。这样就把频繁的磁盘操作转换为对内存的读写,效率会大大提高。
对于缓冲文件系统,系统会自动地为每一个正在使用的文件开辟一块缓冲区,缓冲区的大小一般由C语言的版本决定,一般的为0.5KB,即512byte。文件是保存在磁盘上的,磁盘数据的组织方式按扇区进行,所以把缓冲区大小定为512byte,与磁盘的一个扇区大小相同,保证了磁盘操作的效率。当要把数据写入磁盘时,先把数据写入内存缓冲区,一旦写满了512byte,操作系统会自动把全部数据写入磁盘的一个扇区,然后把内存缓冲区清空,新的数据继续写入内存缓冲区。当要从磁盘中读取数据时,系统先把一个扇区的数据导入内存缓冲区,供C程序读取,一旦512byte数据都被读入,系统会自动把下一个扇区的数据导入内存缓冲区,以供C程序继续读入数据。这样做的目的是减少对磁盘的实际读写次数,因为每一次读写都要移动磁头并寻找磁道扇区,花费了一定时间。缓冲文件系统是借助结构体指针来对文件进行管理,通过文件指针对文件进行访问,既可以读写字符、字符串、格式化数据,也可以读写二进制数据,缓冲文件系统函数一般是由f开头的函数,例如fopen()、fwrite()、fread()。
对于非缓冲文件系统,内存缓冲区不会由系统自动分配,而是需要编程者根据自己的需要,在程序中使用C语句进行分配。不同的操作系统对文件的处理会有些相应的规定。在UNIX操作系统中,用缓冲文件系统来处理文本文件,用非缓冲文件系统处理二进制文件,而标准ANSI C中规定只采用缓冲文件系统。非缓冲文件系统依赖于操作系统,通过操作系统的功能对文件进行读写,是系统级的输入输出,它不设文件结构体指针,只能读写二进制文件,但效率高,速度快。
一般将从内存往外存写数据称为“输出”,从外存中读取数据称为“输入”。

文件控制块
FCB(File Control Block):文件缓冲区与磁盘文件之间的处理是由操作系统自动完成的,操作系统通过文件控制块来进行磁盘文件操作。文件控制块包括文件属性、文件名、驱动器号、扩展名、文件长度和文件纪录状态等信息。操作系统通过文件表来管理文件。它给每个文件顺序编号,并对应一个不同的FCB。程序要访问文件时,用一个FILE指针指向文件缓冲区,此时操作系统会把文件缓冲区与FCB相关联,FCB又直接对应于磁盘。

对文件的操作

引例
在C盘中有一个JackeySong.txt的文件,里面存储的信息如下:
202101 小明 91
202102 Amy 85
202103 Petter 76
202104 Jenny 69
202105 JackeySong 55
现要求用C语言读取该文件里面的数据,并计算平均成绩:

#include <stdio.h>      //包含了结构体FILE
int main(){
	FILE *fp;      //定义文件指针
	int num;
	char stname[20];
	int i, score;
	int avg_score = 0;
	if((fp = fopen("C:\\JackeySong.txt","r"))==NULL){
		// fp = fopen("文件路径","文件的操作方式"),fopen()是一个返回指针值的函数
		printf("FIle open error!\n");
		exit(0);
	}       //读取文件
	for(i = 0; i < 5; i++){
		fscanf(fp,"%d%s%d",&num,stname,&score);
		avg_score += score;
		printf("%d %s %d\n",num,stname,score);
	}        //输出文件里面的信息
	printf("Average score:%d\n",avg_score/5);    //输出平均分
	if(fclose(fp)){
		printf("Can not close the file!\n");
		exit(0);
	}     //关闭文件
	return 0;
}

运行结果:
在这里插入图片描述
如果汉语的“小明”输出时出现了乱码,注意检查文本文件的编码方式。我这里采用的是ANSI编码:
在这里插入图片描述
如果出现了乱码,文件 ——> 另存为:
在这里插入图片描述
在这个界面选择编码模式,重新保存即可。多尝试几种编码模式。正常情况下ANSI编码不会乱码,如果是UTF-8编码可能会出现乱码情况。


文件指针

缓冲文件系统通过FILE指针实现对文件的操作,利用FILE指针,既可以读写字符和字符串,也可以按照指定的格式进行读写。可以顺序读,也可以随机读。

C语言对文件进行操作时,需要了解:

  • 文件在磁盘中的存储位置
  • 文件的类型(文本文件或二进制文件)
  • 对文件进行什么操作(读或写)
  • 对文件以什么方式进行读写(读一个字符或读一个字符串)
  • 该文件调入到内存缓冲区的地址

缓冲文件系统为文件定义了一个结构体变量,使用typedef命名了一个别名为FILE,该结构体变量中包含了要读写文件的基本信息。FILE结构体类型一般定义在stdio.h头文件中,在使用前我们只需要#include<stdio.h>即可。

typedef struct {
	short level;	//缓冲区使用量
	unsigned flags;	//文件状态标志
	char fd;	//文件描述
	short bsize;	//缓冲区大小
	unsigned char *buffer;	//文件缓冲区的首地址
	unsigned char *curp;	//指向文件缓冲区的工作指针
	unsigned char hold;	//无缓冲则不读取字符
	unsigned istemp;	//临时文件指示器
	short token;	//用于有效性检查
}FILE;

文件的打开与关闭

打开文件实际上是将文件从磁盘调入到内存缓冲区中的操作。对文件读写完毕后,必须将文件关闭,关闭文件实际上是释放文件所占的缓冲区空间的操作。

fopen()函数

在使用fopen()函数之前,要先定义一个文件指针FILE *fp;,然后调用fopen()函数打开文件,函数返回值是一个指针值,将这个指针值赋值给指针变量fp,fopen()函数有两个参数,第一个参数为要打开的文件的位置和文件名,第二个参数为文件操作的方式。如果成功打开文件,则fopen()函数返回该文件的指针;如果该文件不存在,不能成功打开文件,则返回空指针NULL(即0)。例如:

FILE *fp;
fp = fopen("C:\\Users\\JackeySong\\Desktop\\a.txt","r");
//注意一点字符串里面的反斜杠 \ 需要使用转义字符 \\

注意一点字符串里面的反斜杠 \ 需要使用转义字符 \\

文件操作方式表
文件操作方式含义
"r"(只读)以只读方式打开一个字符文件
"w"(只写)以只写方式打开一个字符文件,文件指针指向文件首部
"a"(追加)打开字符文件,指向文件末尾,在文件末尾追加数据
"rb"(只读)以只读方式打开一个二进制文件
"wb"(只写)以只写方式打开一个二进制文件
"ab"(追加)打开二进制文件,指向文件末尾,在文件末尾追加数据
"r+"(读写)以读写方式打开一个已存在的字符文件
"w+"(读写)以读写方式新建一个字符文件
"a+"(读写)以追加方式打开一个字符文件
"rb+"(读写)以读写方式打开一个已存在的二进制文件
"wb+"(读写)以读写方式新建一个二进制文件
"ab+"(读写)以追加方式打开一个二进制文件

注:

  • "r""rb"只能从文件中读取数据,而不能写入,且文件必须已经存在。如果使用"r""rb"打开一个不存在的文件,那么文件指针fp的值将会是一个NULL
  • "w""wb"只能往文件中写入数据,而不能读出。如果要打开的文件不存在,则新建一个与要打开文件名字相同的文件;如果要打开的文件存在,则新建一个空文件覆盖掉已存在的文件。
  • "a""ab"的方式打开文件时,如果文件存在,就在文件末尾追加;如果文件不存在,则新建一个文件在开头处追加。

fclose()函数

对文件执行完操作后,需要将文件关闭,把更新后的数据写回硬盘,并释放该文件所占内存缓冲区的空间。若文件不关闭,则有可能文件被误用,造成信息混乱或丢失。

fclose()函数的参数为文件指针,关闭该指针所指向的文件。如果关闭成功,返回值为0;若不能正常关闭文件,返回值非零。不能正常关闭文件可能是因为写入磁盘不成功,或文件正被其他程序占用,或试图关闭一个已经关闭了的文件,或者该文件无效等。对于不用的文件要及时关闭,防止数据丢失。

文件读写

文件打开后,才能进行读写操作。

fputc()函数

功能是将一个字符写入到文件中,原型:int fputc(int ch, file *fp);。fputc()函数将把一个字符 ch 输出到 fp 所指向的文件中,如果输出成功,则返回输出的字符;若失败或文件结束,则返回EOF。EOF是End Of File的缩写,表示“文字流(stream)”的结尾,这里的“文字流”可以是文件file,也可以是标准输入stdin。

fputc()的调用格式fputc(ch,fp);,ch是要输出的字符,可以是一个字符常量,也可以是一个字符变量,fp为文件指针。例如:

fputc('d',fp);

char a = 'd';
fputc(a,fp);

将字符d输出到fp所指向的文件中。

注意 fputc() 函数也是有返回值的:

char ch;
ch = fputc('e',fp);    //如果输出成功,fputc()返回值为输出的字符,ch将等于字符e
FILE *fp = NULL;    //文件指针为空
if((fputc('a',fp))==EOF)//因为fp为空,所以将字符a输出到fp一定会输出失败,fputc()函数的返回值为EOF
	printf("error!");

fputc()函数和putchar()函数的功能类似,putchar()是fputc()的一个特例,putchar()的功能可以由fputc()代替,在屏幕上输出字符d:

putchar('d');

fputc('d',stdout);     //stdout表示标准输出设备,一般为显示器

fgetc()函数

原型:int fgetc(file *fp);。fgetc()函数是从文件中读出一个字符,如果成功,返回输入的字符;若失败或文件结束,返回EOF。注意fgetc()函数也是有返回值的,可以单独直接调用这个函数,也可以将其返回值赋值给其他变量。EOF的值为-1,它是一个提前定义好的无参宏的宏定义#define EOF -1。关于宏定义是前面的文章中说过的。

fgetc()函数的一般调用格式为fgetc(fp);,也可以用变量接收其返回值ch = fgetc(fp);

fgetc()函数和getchar()函数功能也是类似的,getchar()函数的功能也可以被fgetc()函数替代,从键盘输入一个字符:

char ch;
ch = getchar();

char ch;
ch = fgetc(stdin);   //stdin表示标准输入设备,一般指键盘

补充feof()函数

该函数的功能为检查文件是否结束,如果没有结束,返回0;如果结束,返回一个非零值。其参数为文件指针。例如:

char ch;
FILE *fp;
fp = fopen("一个文件路径","r");    //只读方式打开文件
ch = fgetc(fp);
while(!feof(fp)){//文件没有结束时输出字符,feof(fp)文件没有结束返回0,然后!0,取反就是1;如果文件结束,feof(fp)返回值非零,取反后就是0
	putchar(ch);
	ch = fgetc(fp);
}
fclose(fp);

fputs()函数

函数原型:int fputs(char *str, FILE *fp);。fputs()函数把一个字符指针str所指向的字符串输出到fp所指向的文件中,若输出成功,返回输出字符个数(或最后的字符);若失败,返回EOF。

调用格式:

char a[20] = "JackeySong";
FILE *fp;
fp = fopen("文件路径","w");
fputs(a,fp);      //将字符数组名作为参数
fclose(fp);

char *str = "JackeySong";      //让字符指针指向字符串JackeySong
FILE *fp;
fp = fopen("文件路径","w");
fputs(str,fp);       //将字符指针作为参数
fclose(fp);

FILE *fp;
fp = fopen("文件路径","w");
fputs("JackeySong",fp);       //将字符常量作为参数
fclose(fp);

在输出的时候,字符串的结束标志'\0'不会输出到文件,也不会在字符串末尾自动添加换行。

fputs()函数和puts()函数也类似,也可以完全用fputs()函数代替puts()函数:

fputs("JackeySong",stdout);

fgets()函数

原型:char *fgets(char *str, int n, FILE *fp);。功能为从指针fp所指向的文件中读取n-1个字符,并把它送到由指针str所指向的字符数组中。若读取成功,返回str首地址,否则返回NULL。读取时,读完指定的n-1个字符自动添加一个结束符并返回。若不足n-1个字符就遇到'\n'或文件结束EOF,停止读入,'\n'也作为一个字符读入。

#include <stdio.h>
int main(){
	char s[10];
	fgets(s,10,stdin);     //stdin指从键盘输入9个字符
	printf("%s",s);
	printf("c");
	return 0;
}

从键盘输入若干行文本(不超过64个字符),写入到C盘LikeC.txt文件中,以-1为输入结束标志:

#include <stdio.h>
#include <string.h> 
int main(){
	FILE *fp;
	int n;
	char buffer[64];
	if((fp = fopen("C:\\LikeC.txt","w"))==NULL){
		printf("can't open file\n");
		exit(1);
	}
	while(strcmp(gets(buffer),"-1")!=0){   //当输入的文本不等于-1时
		fputs(buffer,fp);      //写入一行
		fputs("\n",fp);        //gets()函数不会读入\n,所以要手动加上换行的转义字符
	}
	fclose(fp);
	
	return 0;
}

注意gets()函数是从键盘输入一个字符串,以回车键为输入结束标志,但'\n'不会输入。gets()也是有返回值的,返回的是一个字符指针,指向buffer字符数组。


fprintf()函数

该函数实现文件的格式化写入操作,即文件的格式化输出。原型:int fprintf(FILE *fp, char *format[,argument,...]);

fprintf()函数的功能为格式化往文件指针fp所指向的文件中输出数据:

//向C盘的文件中写入字符串“Hello everyone! My name is JackeySong. 666”
	FILE *fp;
	char a[10] = "JackeySong";
	int b, c;
	b = c = 6;
	if((fp = fopen("C:\\LikeC.txt","w"))==NULL){
		printf("file open error!");
		exit(1);
	}
	fprintf(fp,"Hello everyone! My name is %s. %d%d%d",a,b,c,6);
	fclose(fp);

fscanf()函数

fscanf()函数实现文件的格式化读取操作。函数原型:int fscanf(FILE *fp, char *format[,address,...]);

例如:

int a,b;
fp = fopen("文件路径","r");
fscanf(fp,"%d,%d",&a,&b);
printf("%d,%d",a,b);

fscanf()函数也可以代替scanf()函数,fprintf()函数也可以代替printf()函数:

	int a;
	char b;
	char str[20];
	fscanf(stdin,"%d %c %s",&a,&b,str); 
	fprintf(stdout,"%d %c %s",a,b,str);

fread()函数和fwrite()函数

fread()函数
函数原型:int fread(void *buffer, unsigned int size, unsigned int n, FILE *fp);。调用格式:fread(buffer,size,count,fp);,其中buffer为一个指针,是读入数据存放的地址。size读取每个数据所占的字节数。count读写大小为size数据的个数。fp文件指针,指向要读取的文件。 fread()函数的功能为:从文件指针fp所指向的文件中,读取count个大小为size字节数的数据项,存放到以buffer为首地址的内存区域中。完成一个读取操作后,如果没有关闭文件,则文件指针fp自动后移动前一次读写的长度,准备继续读写下一个数据块。

fwrite()函数
原型:int fwrite(const void *buffer,unsigned int size,unsigned int n,FILE *fp);。调用格式:fwrite(buffer,size,count,fp);,功能为往文件指针fp所指向的文件中写入count个大小为size字节数的数据项,该数据项在内存中的首地址为buffer。

fread()函数和fwrite()函数读写时,都应采用二进制模式,且一般都是对结构体数据的读写。例如:

fread(buffer,10,sizeof(STU),fp);    //STU为一个结构体的别名
fwrite(s,sizeof(STU),5,fp);

输入学生信息,写入文件,再读取输出到显示器

#include <stdio.h>
typedef struct {
	int num;
	char name[8];
	float uscore;
	float tscore;
	float fscore;
}STU;      //typedef为结构体定义了一个别名STU
#define size sizeof(STU)      //宏定义,size的大小为结构体STU所占的字节数
int main(){
	STU s[3],t[3];
	int i;
	FILE *fp;
	for(i = 0; i < 3; i++){       //分别输入三个学生的信息
		printf("Please input the number:\n");
		scanf("%d",&s[i].num);
		printf("Please input the name:\n");
		scanf("%s",s[i].name);
		printf("Please input usually score & test score:\n");
		scanf("%f%f",&s[i].uscore,&s[i].tscore);
		s[i].fscore = s[i].uscore*0.2+s[i].tscore*0.8;
	}
	fp = fopen("D:\\data","wb");     //二进制只写方式写入D盘二进制文件data中
	for(i = 0; i < 3; i++)
		fwrite(&s[i],size,1,fp);
	fclose(fp);
	fp = fopen("D:\\data","rb");     //二进制只读读取刚刚写入的信息
	for(i = 0; i < 3; i++)
		fread(&t[i],size,1,fp);
	for(i = 0; i < 3; i++)
		fprintf(stdout,"%d %s,%f,%f,%f\n",t[i].num,t[i].name,t[i].uscore,t[i].tscore,t[i].fscore );
	fclose(fp); 
	return 0;
}

运行结果:
在这里插入图片描述

文件定位函数

以上所有对文件读写的函数都是顺序读写,每次读写文件,文件位置指针都会相应地向后移动。为了能够读写文件指定位置的数据,就要先把文件指针移动到指定位置,即文件的定位。就要使用文件定位函数:

fseek()函数

原型:int fseek(FILE *fp,long offset,int origin);。功能:实现将文件指针移动到指定位置。调用格式为fseek(fp,offset,origin);,其中fp为文件指针。offset为移动的字节数,正数表示向后移动,负数表示向前移动,移动单位为字节。origin为基准位置,0代表文件开头(SEEK_SET),1代表当前位置(SEEK_CUR),2代表文件末尾(SEEK_END)。例如:

fseek(fp,10,0);   //意思是从文件开头向后移动10个字节
也可以写成:
fseek(fp,10L,0);    // 这里的大写字母 L 意思是 long

再例如:

fseek(fp,-10,2);    //意思是从文件末尾向前移动10个字节

注意fseek()函数也是可以有返回值的,如果移动成功返回0,否则返回-1。

以下示例在Linux操作系统下演示:

#include <stdio.h>
int main(){
    FILE *fp;    //定义文件指针
    int n;       //定义一个整数
    char str[80];
    fp = fopen("/home/jackeysong/Desktop/test","w+");    //打开桌面的文件
    fprintf(fp,"fasdfasdfjlkasdjlfkajsd;fhwoiehgpoaiwejfo;alsijhf");      //写入这一串字符
    n = fseek(fp,-15,2);       //将指针从文件末尾处向前移动15个字节
    fprintf(fp,"JackeySong");      //写入JackeySong字符
    printf("%d\n",n);    // n接收了函数fseek()的返回值,结果是0
    fseek(fp,0,0);    //再次将指针定位到文件开头,以进行下面的读取数据
    fscanf(fp,"%s",str);    //从文件读取字符串赋值给str
    fprintf(stdout,"%s\n",str);     //将读取的字符串输出到显示器
    fclose(fp);    //关闭文件
    return 0;
}

运行结果:
在这里插入图片描述

ftell()函数

原型:long ftell(FILE *fp);,作用是:返回当前读写位置偏离文件开头的字节数。若成功,返回当前文件指针位置;若出错,返回-1。

Windows操作系统下,在C盘文件的 data.txt 文本文件里面已经写入了以下内容:

Get busy living, or get busy dying.
要么忙着活,要么忙着死。
如果不是为了自己想要的生活而忙碌,那就是在忙着一天天走向死亡。

编程计算该文本文件所占的字节数:

#include <stdio.h>
int main(){
	FILE *fp;
	int fsize;
	fp = fopen("C:\\data.txt","rb");
	fseek(fp,0,SEEK_END);    //文件指针移动到文件末尾 
	fsize = ftell(fp);
	printf("The length of the file is %d byte\n",fsize);
	fclose(fp); 
	return 0;
}

运行结果:
在这里插入图片描述
168个字节可能会有人质疑,怀疑代码是否写错。data.txt文本文件我是用的UTF-8编码,在UTF-8编码中一个汉字占三个字节。所以要比ASCII编码大了很多个字节。

rewind()函数

功能是将文件位置指针重新返回文件开头,原型:void rewind(FILE *fp);,其中 fp 为文件指针。调用格式:rewind(fp);

补充ferror()函数和clearerr()

ferror()函数: 检查文件在用各种输入输出函数进行读写时是否出错,返回值为0表示未出错,否则表示出错。调用格式:ferror(fp);

clearerr()函数: 清除出错标志和文件结束标志,是它们值为0。调用格式:clearerr(fp);

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Jackey_Song_Odd

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值