C语言——文件操作

430 阅读12分钟

为什么使用文件

文件是存放在硬盘上的。当数据存放在内存中时,程序结束,数据就会丢失;

若想实现数据持久化,就要把数据存放在电脑硬盘上.

文件分类

程序文件:

源程序文件(例:后缀为.c)、目标文件(例:windows下后缀为.obj)、可执行程序.

数据文件:

我们写的源文件中的代码可能会涉及操作一个文件,比如向文件中写入内容,

该文件的内容是程序运行时读写的内容,该文件就是数据文件

例:

image.png

文件名

一个文件要有唯一的文件标识.

包含3部分:文件路径 + 文件名主干 + 文件后缀

例:C:\code\test.txt

(C盘里的一个文本文件)

文件信息区

每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息

(文件名、文件状态等)

这些信息是保存在一个结构体变量中,该结构体类型是系统声明,取名FILE.

image.png

打开一个文件时,

系统会根据文件的情况自动创建一个FILE类型变量,并自动填充其中信息,不用关心细节.

文件指针/文件类型指针

一般通过FILE指针来维护这个结构体变量.

FILE* pa;//文件指针变量

通过文件指针可以找到某个文件的文件信息区,而文件信息区又强行与文件相关联,

所以可以通过文件指针进行文件操作.

文件的打开和关闭

要给瓶子灌水/放水,总要先打开瓶盖,最后拧上瓶盖,文件操作也是如此.

文件在读写之前应先打开文件,使用结束后应关闭文件.

ANSIC规定用fopen函数来打开文件,用fclose函数关闭文件.

fopen函数

原型:

FILE* fopen(const char* filename, const char* mode)

用法:

filename是文件名,mode是打开方式.

部分打开方式如下:

打开方式含义若文件不存在
"r"(只读)打开一个已经存在的文本文件出错
"w"(只写)打开一个文本文件,若文件存在则销毁文件中的内容建立一个新文件
"a"(追加)向文本文件尾部添加数据建立新文件

例:

FILE* pa = fopen("C:\\code\\test.txt", "r")//两个\转义为一个\

若文件存在:fopen函数会在内存中创建与该文件C:\code\test.txt相关联的文件信息区,

把文件信息区填好,并返回该文件信息区的起始地址.

若文件不存在:打开失败,返回空指针NULL.

    FILE* pa = fopen("C:\\code\\test.txt", "r");
    if(NULL == pa)//判空操作
    {
        perror("fopen");
        return 0;
    }
    else
    …………………………

fclose函数

原型: int fclose(FILE* stream)

用法:(接上面的代码)

fclose(pa);//关闭文件
pa = NULL; //pa及时置为空指针

若关闭成功,返回0.

文件内容的读写

我们电脑上有许多外部设备——例如键盘、屏幕、硬盘、网卡......

每种外部设备都有所差异,所以操作这些外部设备的方法不一样.

比如把数据打印到屏幕(把数据写到屏幕上)、从键盘中读取数据......

此时我们还要知道各种设备怎么读写.

为了降低学习成本,设计者在各种外部设备的上层封装了 流.

我们不用关心流怎么把数据放到对应的设备中,或者怎么从对应的设备中获取数据.

要读取信息时,从对应的流里读;要写信息时,把数据写到对应的流中。

流会自动找到对应的外部设备进行读写信息.

例如,我要打印信息到屏幕上——就会先把数据写到标准输出流中,标准输出流会把数据写到屏幕

image.png

注意:一个C程序运行起来,下面三个流是默认打开的,它们的类型竟然都是FILE*.

标准输入流stdin(关联键盘).

标准输出流stdout(关联屏幕).

标准错误流stderr(关联屏幕).

而文件流(关联硬盘中的文件)需要编程者自己打开,即文件指针。////

文件内容的顺序读写

按照一定的顺序进行读或写。

接下来先关注文件流

功能函数适用流
字符输出fputc所有输出流
字符输入fgetc所有输入流
文本输出fputs所有输出流
文本输入fgets所有输入流
格式化输出fprintf所有输出流
格式化输入fscanf所有输入流
二进制输出fwrite文件流
二进制输入fread文件流

什么是输出?把程序的数据传输到外部。(把程序的数据写到流中,例如写文件,把信息写到文件中)

什么是输入?把外部数据传给程序。(从流中读取数据,例如读文件,读取文件信息到程序中)

fputc函数

原型:

int fputc(int c, FILE* stream)

功能:

写一个字符到 流 中.(例:可以把写一个字符到文件中)

使用:

        //以写的方式打开文件
	FILE* pa = fopen("D:\\cde.txt", "w");
	if (pa == NULL)
	{
		perror("fopen");
		return;
	}
	//写内容到文件中去
	fputc('a', pa);//注意会按顺序写abc
	fputc('b', pa);
	fputc('c', pa);
	//关闭文件
	fclose(pa);
	return 0;

image.png

        //再次运行一次,会发现文件之前的内容丢失,因为以"w"方式打开,文件已存在会先销毁文件内容
        FILE* pa = fopen("D:\\cde.txt", "w");
	if (pa == NULL)
	{
		perror("fopen");
		return;
	}
        for (int i = 'k'; i <= 'z'; i++)
	{
            fputc(i, pa);
	}
        flose(pa);
        pa=NULL;
        return 0;

image.png

fgetc函数

原型:

int fgetc(FILE* stream)

功能:

从流中读取一个字符,读取成功则返回字符的ANSIC码值.(可用来读文件)

若读取过程中 遇到一个错误 或者 读到文件末尾,返回EOF(end of file).

image.png

使用:

        //接上面的文件内容——已经写进去'k'~'z'
        FILE* pa = fopen("D:\\cde.txt", "r");//此时改为读文件
	if (pa == NULL)
	{
		perror("fopen");
		return;
	}
        int ch = 0;
	while (( ch = fgetc(pa) ) != EOF)
	{
         //先把fgetc(pa)读到的字符的ASCIC码赋给ch, 再判断ch是否为EOF
         //如果是EOF,循环停止
         //如果不是,把读到的字符打印出来
		printf("%c", ch);
	}

运行效果:

image.png

fputs函数

原型:

int fputs(const char* string, FILE* stream)

功能:

string:要输出的字符串;stream:流.

把一个字符串输出到流中,可以直接把一个字符串写入到文件中(最终效果).

使用:

        FILE* pf = fopen("D:\\cde.txt", "w");
	if (pf == NULL)
	{
		perror("fopen");
		return 0;
	}
	fputs("shxmll", pf);//写一个字符串;
	fputs("ysgz\n", pf);//再写一个字符串,后面加换行符
	fputs("你好世界",pf);
	fclose(pf);

运行效果:

image.png

fgets函数

原型:

char *fgets(char *string,int n, FILE *stream)

功能:

将从流中最多读到的(n-1)个字符放到string中。

string:(存放读取的字符串)的位置

n:最大读取的字符个数

stream:从哪个流中读取数据

返回值:读到的字符串的(存储首地址)

当遇到文件结束标志EOF或一个错误时,会返回NULL

注意:

1 实际上,fgets最多会从文件中读取(n-1)个字符,放到string中,并添加'\0'.

        FILE* pf = fopen("D:\\cde.txt", "r");
	if (pf == NULL)
	{
		perror("fopen");
		return 0;
	}
	char arr[20] = "xxxxxxxxxx";
	fgets(arr, 4, pf);
        fclose(pf);

image.png image.png image.png

2 fgets一次只读一行,但文本文档中第一行后还隐含一个换行字符,所以fgets也把它读进arr。

image.png image.png image.png

使用:

        FILE* pf = fopen("D:\\cde.txt", "r");
	if (pf == NULL)
	{
		perror("fopen");
		return 0;
	}
	char arr[50] = { 0 };
	while (fgets(arr, 50, pf) != NULL)//用一次fgets后,下一次用就会读取文件的下一行
	{
                //只要不返回NULL,读取成功
		printf("%s", arr);
	}
        fclose(pf);

运行效果

最后一行没放'\n'

image.png

fprintf函数

原型:

int fprintf(FILE *stream, const char *format[,argument]...)

功能:

可以将格式化的数据打印/写到所有输出流中。

类比printf, printf是将格式化的数据打印/写到屏幕中(标准输出流)

例:

    typedef struct S//先声明一个学生结构体
    {
            char name[10];
            int age;
            double weight;
    }S;
    int main()
    {
           FILE* pf = fopen("text3.txt", "w");//在源文件路径下创建一个文件text3.txt
           if (pf == NULL)
           {
                 perror("fopen");
                 return 1;
           }
           S stu = { "xm", 20, 55.5 };
           //把内容写到文件上
           fprintf(pf, "%s %d %.2lf", stu.name, stu.age, stu.weight);
           //把内容写到屏幕上
           printf("%s %d %.2lf", stu.name, stu.age, stu.weight);
           return 0;
    }

运行效果:

image.png

image.png

注意:

fprintf的格式化输出之间没有空格,那么打印到文件中也是密密麻麻的。

fscanf函数

原型:

int fscanf(FILE *stream, const char *format[,argument]...)

功能://

可以从所有输入流中,读取格式化的数据。

类比scanf, scanf可以从键盘中读取格式化的数据。

例:

        S stu = { 0 };//初始化学生结构体变量为0
        
        //从之前的text3.txt中读取信息
	fscanf(pf, "%s%d%lf", stu.name,&(stu.age), &(stu.weight));
        
        //打印到屏幕上
	fprintf(stdout, "%s %d %.2lf", stu.name, stu.age, stu.weight);

运行效果:

image.png

fwrite函数

原型:

size_t fwrite(const void *buffer, size_t size, size_t count, FILE *stream)

功能:

将buffer这块空间的count个size字节大小的数据 以二进制的方式 写到流中

size_t size —— 一个数据的大小(单位byte)

size_t count —— 要写多少个数据

注意:

以二进制的方式写入,之后就要以二进制的方式读取,必须严格匹配.

使用:

typedef struct Student
{
	char name[10];
	int age;
	double weight;
}Student;
int main()
{
	//"wb"是以二进制的方式写文件,若文件存在也会先销毁文件内容
	FILE* pf = fopen("text3.txt", "wb");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	Student stu = { "veri", 35, 66.6 };
	fwrite(&stu, sizeof(Student), 1, pf);
	fclose(pf);
	return 0;
}

用记事本打开文件后,会发现有很多内容很古怪,这是因为记事本不用来读取二进制内容。

但之后用二进制的方式读取文件也能读取到有效信息

image.png

fread函数

原型:

size_t fread(void *buffer, size_t size, size_t count, FILE *stream)

功能:

以二进制的方式从 流 读取count个size字节大小的数据放到buffer中。

返回值为成功读取的数据个数【所以size和count不能写反】

使用:

        //"rb"是以二进制方式读取文件
	FILE* pf = fopen("text3.txt", "rb");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	Student stu = { 0 };
	while( fread(&stu, sizeof(Student), 1, pf) == 1)//每次读取一个数据(每个数据大小是sizeof(Student)),读取成功返回1,失败返回0
            printf("%s %d %.2lf", stu.name, stu.age, stu.weight);
        fclose(pf);    

运行效果:

打印出之前二进制写入的内容

image.png

文件内容的随机读写

以上的函数都默认从文件的第一行开始有序读写数据。

文件内部指针

在操作一个文件进行读写时,会有一个内部指针指向下一个将要读写的位置。

默认在打开文件时,该文件内部指针是指向第一行第一列的。

每进行一次读写时,该内部指针会自动移动,指向下一次读写的位置。

如果能改变文件内部指针的位置,就能实现读写任意地方的内容

fseek函数

原型:

int fseek(FILE* stream, long offset, int origin)

功能:

概括:移动文件内部指针到特定的位置

origin 有3个值

1、 SEEK_CUR 文件内部指针当前位置

2、 SEEK_END 文件末尾

3、 SEEK_SET 文件开始位置

offset —— 偏移量。若为正,代表向右偏移;为负,向左偏移

例:

//pf和pa是2个不同的文件指针

feek(pf, 3, SEEK_CUR);//将文件内部指针向右移3个单位

feek(pa, -2, SEEK_END);//将文件内部指针移到文件末尾向左2个单位的位置

注意:

fseek只是把文件内部指针移到指定的位置,并不会进行读写;

但如果以追加方式打开文件,feek不起作用。

使用:

要读取的文件

image.png

        FILE* pf = fopen("D:\\cde.txt", "r");
	char re1 = fgetc(pf);
	char re2 = fgetc(pf);
	fseek(pf, 3, SEEK_CUR);//使文件内部指针向右偏离3个字符
	char re3 = fgetc(pf);
	printf("%c%c%c", re1, re2, re3);

输出效果

image.png

拓展:

文件末尾位置:可以理解为 【没有任何数据可以读写】的开始位置

        FILE* pf = fopen("D:\\cde.txt", "w");
	fputc('a', pf);
	fseek(pf, 3, SEEK_CUR);//使文件内部指针向右偏离3个字符
	fputc('c', pf);
	fseek(pf, 3, SEEK_END);//使文件内部指针移到文件结束的后面3个字符
	fputc('k', pf);
	//注意:文件里并不存在EOF,这只是一种状态,在没有可读取的数据时,还想读取数据,
	//就会返回EOF,但写文件就可以在没有数据的地方写
	fclose(pf);

程序运行后文件情况:

image.png

rewind函数

原型:

void rewind(FILE* stream)

功能:

将文件内部指针移动到文件开始位置。

实际上也能用 fseek(pf, 0, SEEK_SET)代替,但rewind(pf)更简便.

ftell函数

原型:

long ftell(FILE* stream)

功能:

返回文件内部指针当前位置离文件开始位置的偏移量.

文件读取完成的判断

feof函数

原型:

int feof(FILE* stream)

功能:

判断当前文件内部指针是否指向文件末尾。是:返回非0值;否:返回0。

通常用来判断读取文件完成后,是读到文件末尾结束,还是遇到了错误提前结束。

例:

image.png

        FILE* pf = fopen("D:\\cde.txt", "r");
	char w = 0;
	while ( ( w=fgetc(pf) ) != EOF)
	{
		printf("%c\n", w);
	}
	if (feof(pf) == 0)
	{
		printf("读取文件过程中遇到错误!!!\n");
	}
	else
	{
		printf("正常读取文件\n");
	}
	fclose(pf);

image.png

怎么判断文件全部数据读取完成?

1、文本文件读取结束:

fgetc的返回值是EOF,fgets的返回值是NULL

2、二进制文件读取结束:

fread的返回值小于读取的数据个数。

    //一般用循环一个数据一个数据来读,读到返回1;没有读到,就读取结束返回0
    while(fread(&stu, sizeof(Student), 1, pf) == 1)
    {
        printf(".....");
    }

文件缓冲区

系统自动为正在使用的文件开辟一块文件缓冲区【不同于文件信息区】

从内存向硬盘中的文件输出数据时(写文件时),会把数据先写到文件缓冲区中,

装满缓冲区后(或刷新缓冲区)才会输出到文件中;

读取文件时,会把读到的数据先放到缓冲区中,

装满缓冲区后(或刷新缓冲区)才会把数据逐个给对应的程序变量

但输入和输出的缓冲区不一样

image.png

        //证明缓冲区的存在
        FILE* pf = fopen("D:\\cde.txt", "w");
	fputs("abcde", pf);
	Sleep(10000);//程序睡眠10秒,在这10秒的时间里打开文件,会发现文件并没有写入数据
	printf("缓冲区刷新完成\n");//此时再打开文件,文件才会有数据
	fclose(pf);//fclose也会刷新文件缓冲区

为什么要及时关闭文件?fclose会刷新文件缓冲区,防止有数据在内存的缓冲区中,导致数据丢失。

当然也能用fflush(pf)手动刷新文件缓冲区。