当前位置: 首页 > news >正文

C语言--文件操作

第16讲:文件操作

1. 为什么使用文件?

如果没有文件,我们写的程序的数据是存储在电脑的内存中,如果程序退出,内存回收,数据就丢失了,等再次运行程序,是看不到上次程序的数据的,如果要将数据进行持久化的保存,我们可以使用文件。

2. 什么是文件?

磁盘(硬盘)上的文件是文件。
但是在程序设计中,我们一般谈的文件有两种:程序文件、数据文件(从文件功能的角度来分类的)。

2.1 程序文件

程序文件包括源程序文件(后缀为.c),目标文件(Windows环境后缀为.obj),可执行程序(Windows环境后缀为.exe)。

2.2 数据文件

文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件。

本章讨论的是数据文件。

在这里插入图片描述

在以前各章所处理数据的输入输出都是以终端为对象的,即从终端键盘输入数据,运行结果显示在显示器上。

其实有时候我们会把信息输出到磁盘(磁盘的文件中)上,当需要的时候再从磁盘上把数据读取到内存中使用,这里处理的就是磁盘上的文件。

在这里插入图片描述

2.3 文件名

一个文件要有一个唯一的文件标识,以便用户识别和引用。

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

例如: c:\code\test.txt

文件路径:c:\code\

文件名主干:test

文件后缀:.txt

为了方便起见,文件标识常被称为文件名。

3. 二进制文件和文本文件?

根据数据的组织形式,数据文件被称为文本文件或者二进制文件
数据在内存中以二进制的形式存储,如果不加转换的输出到外存的文件中,就是二进制文件
如果要求在外存上以ASCII码的形式存储,则需要在存储前转换。以ASCII字符的形式存储的文件就是文本文件

一个数据在文件中是怎么存储的呢?

字符一律以ASCII形式存储,数值型数据既可以用ASCII形式存储,也可以使用二进制形式存储。
如有整数10000,如果以ASCII码的形式输出到磁盘,则磁盘盘中占用5个字节(每个字符一个字节),而
二进制形式输出,则在磁盘上只占4个字节。

在这里插入图片描述

测试代码:

#include <stdio.h>
int main()
{int a = 10000;FILE* pf = fopen("test.txt", "wb");//wb:⼆进制的形式写到⽂件中 fwrite(&a, 4, 1, pf);fclose(pf);pf = NULL;return 0;
}

在VS上打开二进制文件:

在这里插入图片描述

1000在二进制文件中(显示数据在内存中的存储形式(以字节为单位的16进制格式并按照小端字节排序的形式)):(从00000000之后才是数据内容)

image-20250211214949452

由于0000 0000 0000 0000 0010 0111 0001 0000在内存中以小端字节序的存储方式存储,所以在内存中的存储形式就是10 27 00 00

注意:因为计算机在内存中以字节为单位以16进制为格式存放数据,一个字节为8个比特位,所以这里的小端存储也是以字节为单位逆着排序,而一个字节内部的顺序并不发生调换,所以二进制中的10 27进过小端存储的调整后并没有变成01 72
在这里插入图片描述

4. 文件的打开和关闭

4.1 流和标准流

4.1.1 流

我们程序的数据需要输出到各种外部设备(文件、光盘、软盘、U盘···),也需要从外部设备获取数据,不同的外部设备的输入输出操作各不相同,为了方便程序员对不同的外部设备进行方便的操作,我们抽象出了流的概念,我们可以把流想象成流淌着字符的河。

C程序针对文件、画面、键盘等的数据输入输出操作都是通过流操作的。

一般情况下,我们要想向流里写数据,或者从流中读取数据,都是要打开流,然后操作。

4.1.2 标准流

那为什么我们从键盘输入数据,向屏幕上输出数据,并没有打开流呢?
那是因为C语言程序在启动的时候,默认打开了3个流:

  • stdin - 标准输入流,在大多数的环境中从键盘输入,scanf函数就是从标准输入流中读取数据。
  • stdout - 标准输出流,大多数的环境中输出至显示器界面,printf函数就是将信息输出到标准输出流中。
  • stderr - 标准错误流,大多数环境中输出到显示器界面。

这是默认打开了这三个流,我们使用scanf、printf等函数就可以直接进行输入输出操作的。

stdin、stdout、stderr三个流的类型是:FILE,通常和你为文件指针

C语言中,就是通过FILE*的文件指针来维护流的各种操作的。

4.2 文件指针

缓冲文件系统中,关键的概念是“文件类型指针”,简称“文件指针”。

每个被使用的文件(包括外部设备)都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件的名字,文件状态及文件当前的位置等)。这些信息是保存在一个结构体变量中的。该结构体类型是由系统声明的,取名 FILE

例如,VS2013编译环境提供的stdio.h头文件中有以下的文件类型申明:

struct _iobuf {char *_ptr;int _cnt;char *_base;int _flag;int _file;int _charbuf;int _bufsiz;char *_tmpfname;
};typedef struct _iobuf FILE;

不同的C编译器的FILE类型包含的内容不完全相同,但是大同小异。

每当打开一个文件的时候,系统会根据文件的情况自动创建一个FILE结构的变量,并填充其中的信息,使用者不必关心细节。

一般都是通过一个FILE的指针来维护这个FILE结构的变量,这这样使用起来更加方便。

下面我们可以创建一个FILE*的指针变量:

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

定义pf是一个指向FILE类型数据的指针变量。可以使pf指向某个文件的文件信息区(是一个结构体变量)。通过该文件信息区中的信息就能够访问该文件。也就是说,通过文件指针变量能够间接找到与它关联的文件

每一个文件都有一个属于自己的文件信息区,文件放在硬盘中,而文件信息区在内存中。

比如:

在这里插入图片描述

4.3 文件的打开和关闭

文件在读写之前应该先打开文件,在使用结束之后应该关闭文件
在编写程序的时候,在打开文件的同时,都会返回一个FILE*的指针变量指向该文件,也相当于建立了指针和文件的关系。
ANSIC规定使用fopen函数来打开文件,fclose来关闭文件。

//打开⽂件 
/*
参数:filename:文件名mode:打开方式
返回值:文件信息区的地址(如果打开文件成功返回有效指针,打开文件失败则返回NULL)
*/
FILE * fopen ( const char * filename, const char *   );//关闭⽂件 int fclose ( FILE * stream );

下面对于“读”“写”的对象,应该有所区分:

在这里插入图片描述

mode表示文件的打开模式,下面都是文件的打开模式:

文件使用方式含义如果指定文件不存在
“r” (只读)为了输入数据,打开一个文本文件出错
“w” (只写)为了输出数据,打开一个文本文件建立一个新的文件夹
“a” (追加)向文本文件尾部添加数据建立一个新的文件夹
“rb” (只读)为了输入数据,打开一个二进制文件出错
“wb” (只写)为了输出数据,打开一个二进制文件建立一个新的文件夹
“ab” (追加)向一个二进制文件尾部添加数据建立一个新的文件夹
“r+” (读写)为了读取和写,打开一个文本文件出错
“w+” (读写)为了读取和写,建立一个新的文本文件建立一个新的文件夹
“a+” (读写)打开一个文件,在文件尾进行读写建立一个新的文件夹
“rb+” (读写)为了读取和写,打开一个二进制文件出错
“wb+” (读写)为了读取和写,建立一个新的二进制文件建立一个新的文件夹
“ab+” (读写)打开一个二进制文件,在文件尾进行读写和写建立一个新的文件夹

补充:

  • “W”:如果文件名存在,会清空文件中的内容。如果文件名不存在,会新建一个此文件名的文件。
  • 如果需要找电脑中别的位置的文件,需要写详细文件的路径,并注意转义字符。
    • C:\Users\Administrator\Desktop\test.txt(绝对路径)
    • .\\..\\..\\test.txt(相对路径)
      • .表示当前路径
      • …表示上一级路径
      • 上面的路径含义为:当前路径底下的上一级路径的上一级路径中的test.txt文件

实例代码:

/* fopen fclose example */
#include <stdio.h>int main ()
{FILE * pFile;//打开⽂件 pFile = fopen ("myfile.txt","w");//⽂件操作 if (pFile!=NULL){fputs ("fopen example",pFile);//关闭⽂件 fclose (pFile);pFile = NULL;}return 0;
}

5. 文件的顺序读写

在文件中按照顺序读写里面的数据。

5.1 顺序读写函数介绍

函数名功能适用于
fgetc字符输入函数(读字符)所有输入流
fputc字符输出函数(写字符)所有输出流
fgets文本行输入函数(读字符串)所有输入流
fputs文本行输出函数(写字符串)所有输出流
fscanf格式化输入函数所有输入流
fprintf格式化输出函数所有输出流
fread二进制输入文件输入流
fwrite二进制输出文件输出流

补充:

  • 上面说的适用于所有输入流一般指适用于标准输入流和其他输入流(如文件输入流);所有输出流一般指适用于标准输出流和其他输出流(如文件输出流)。
  • 上面函数中前6个读写的都是文本信息,并且均适用于所有输出/入流;最后两个读写的是二进制信息,但是仅适用于文件输出/入流。
5.1.1 fgetc和fputc

fgetc函数是在文件顺序读写中用于将文件中的字符一个一个读出的函数。

int fgetc(FILE *stream);

在使用fgect函数读取文件的时候会遇到两种情况:

  • 文件未到末尾,成功读取文件,将读取到的字符的ASCII码值返回
  • 文件未到末尾,读取文件失败,返回EOF(end of file(文件的结束标志))即为-1,便于程序员检查

代码示例:

//代码1:将项目路径底下的test.txt文件中前四位读取出来
int main()
{//打开文件FILE* pf = fopen("test.txt", "r");if (pf == NULL){perror("fopen");return 1;}//读取文件的内容(事先向文件中输入abcdef)printf("%c\n", fgetc(pf));//aprintf("%c\n", fgetc(pf));//bprintf("%c\n", fgetc(pf));//cprintf("%c\n", fgetc(pf));//d//关闭文件fclose(pf);return 0;
}//代码2::将项目路径底下的test.txt文件中a~z全读取出来
int main()
{//打开文件FILE* pf = fopen("test.txt", "r");if (pf == NULL){perror("fopen");return 1;}//读取文件内容int ch = 0;while ((ch = fgetc(pf)) != EOF){printf("%c ", ch);}//关闭文件fclose(pf);pf = NULL;return 0;
}

fputs函数是在文件顺序读写中用于在文件中一个字符一个字符写入文件的函数。

int fputc(int character, FILE *stream);

在使用fputs函数的时候也存在两种情况:

  • 文件写入成功,则返回字符的ASCII码值
  • 文件写入失败,则返回EOF

代码示例:

//代码:向项目路径底下的test.txt文件中写入a~z
int main()
{FILE*pf = fopen("test.txt", "w");if (pf == NULL){perror("fopen");return 1;}//写文件int i = 0;for (i = 'a'; i <= 'z'; i++){fputc(i, pf);}//关闭文件fclose(pf);pf = NULL;return 0;
}

**补充:**标准输入/出流的使用

stdin为标准输入流,但是以读的形式打开文件就是文件输入流。

stdout为标准输出流,但是以写的形式打开文件就是文件输出流。

int main()
{int ch = fgetc(stdin);//从键盘(标准输入流)上读取fputc(ch, stdout);//将字符输出(写)到屏幕(标准输出流)return 0;
}
5.1.2 fgets和fputs

fgets函数原型如下:

char *fgets(char *str, int num, FILE *stream);
  • **函数功能:**从指定的文件流中获取字符串,并将其存储到指定位置
  • 参数:
    • str:用于存储所读到的字符串的位置
    • num:要读取的字符串的字符个数
    • stream:所要读取的文件的文件指针
  • 返回值:
    • 调用成功会返回用于储存数据的位置的起始地址
    • 读取过程中发生错误,或是读取到了文件末尾,则返回一个空指针(NULL)

代码示例:

//读出文件test.txt中的10个字符到数组arr中去,并打印出来
int main()
{//1. 打开文件FILE* pf = fopen("test.txt", "r");if (pf == NULL){perror("fopen");return 1;}//2. 读文件char arr[20] = "xxxxxxxxxxxxx";//用于区分是不是fgetc函数读出的字符fgets(arr, 10, pf);printf("%s\n", arr);//在控制台上仅仅显示前9个字符//3. 关闭文件fclose(pf);pf = NULL;return 0;
}

注意:fgets函数读取字符的过程中会出现两种情况

  1. 在fgets函数读取到换行符(\n)时,则停止读取,此时带回的字符串中包含换行符,并在换行符后再加上一个空字符(\0)。
  2. 在fgets函数读取到num-1个字符都没有读取到\n时,则读取到num-1个字符,并在末尾加上一个空字符一同返回(共n个字符)。

fputs函数原型如下:

int fputs(const char *str, FILE *stream);
  • **函数功能:**写字符串到指定的文件流中去
  • 参数:
    • str:要写入文件的字符串数组的首地址
    • stream:要写入的文件的文件流
  • 返回值:
    • 函数调用成功,输出成功返回一个非负值
    • 函数调用失败,输出时发现错误返回EOF(-1)

代码示例:

//在项目路径底下的test.txt文件中输入“I am a student”"Are you OK??"
int main()
{//1. 打开文件FILE* pf = fopen("test.txt", "w");if (pf == NULL){perror("fopen");return 1;}//2. 写文件fputs("I am a student\n", pf);fputs("are you ok??", pf);//3. 关闭文件fclose(pf);pf = NULL;return 0;
}

同样以上两个函数fgets和fputs也是适用于所有数据流,可以用键盘输入或者在屏幕上输出。

代码示例:

//在控制台上用键盘输入什么,敲回车之后控制台就会显示什么
int main()
{char arr[20] = { 0 };fgets(arr, 20, stdin);fputs(arr, stdout);return 0;
}
5.1.3 printf/fprintf/sprintf
  • printf函数原型:

    int printf(const char *format, ...);
    
  • fprintf函数原型:

    int fprintf(FILE *stream, const char *format, ...);
    
  • sprintf函数原型:

    int sprintf(char *str, const char *format, ...);
    

①、经过观察前两个函数可以看出函数的参数部分只有首个参数不同,因为首个参数表示对某种流进行操作。printf默认操作的是stdout,fprintf操作的是所有输出流,所以在使用fprintf的时候只需要在使用printf函数的基础上加上一个参数即可。同时因为所有输出流包含标准输出流stdout,也就说明printf就是fprintf的一种。

代码示例:

//将下面的结构体分别打印在test.txt文件中和屏幕上
int main()
{struct S s = { "lisi", 18, 88.0f };//1. 打开文件FILE* pf = fopen("test.txt", "w");if (pf == NULL){perror("fopen");return 1;}//2. 写文件fprintf(pf,"%s %d %.1f", s.name, s.age, s.score);	//在文件中打印数据printf("%s %d %f", name, age, score);				//在屏幕上打印数据//3. 关闭文件fclose(pf);pf = NULL;return 0;
}

观察可知sprintf的第一个参数是一个字符指针。而因为printf函数是将后面格式化的数据打印在屏幕上,fprintf函数是将后面格式化的数据写入文件或者其他流中去,同理sprintf函数是将后面格式化的数据写入str指针指向的字符串中去。所以不管sprintf函数后面的格式化数据为任何类型(int/float/结构体···)都会被放进字符串中去,可以理解为将格式化的数据转化成字符串。

在后续的使用中,若遇到需要将许多各种乱七八糟数据类型的数据转化为字符类型可以使用函数sprintf。

代码示例:

struct S
{char name[20];int age;float score;
};int main()
{char arr[100] = { 0 };struct S s = { "wangwu", 23, 66.6f };//临时变量struct S tmp = { 0 };//将s中的各个数据转换成字符串,存放在arr中sprintf(arr, "%s %d %f", s.name, s.age, s.score);//printf("%s\n", arr);//从字符串arr中提取格式化的数据,存放在tmp中sscanf(arr, "%s %d %f", tmp.name, &(tmp.age), &(tmp.score));printf("%s %d %f\n", tmp.name, tmp.age, tmp.score);return 0;
}

②、同时这三个函数的参数最后都有一个**…(可变参数列表)**,表示此函数的参数数量是并不固定的是可变的,如下

printf("hehe\n");						//1个参数
printf("%d", 100);						//1个参数
printf("%d %c", 100, 'x');				//3个参数
printf("%d %c %f", 100, 'x', 3.14f);	//4个参数
5.1.4 scanf/fscanf/sscanf
  • scanf函数原型:

    int scanf(const char *format, ...);
    
  • fscanf函数原型:

    int fscanf(FILE *stream, const char *format, ...);
    
  • sscanf函数原型:

    int sscanf(const char *s, const char *format, ...);
    

①、先看前两个函数可以看出函数的参数部分只有首个参数不同,因为首个参数表示对某种流进行操作。scanf默认操作的是stdin,fscanf操作的是所有输入流;所以在使用fscanf的时候只需要在使用scanf函数的基础上加上一个参数即可。

代码示例:

//分别从键盘和和文件中读取数据,并将其显示在屏幕上
int main()
{struct S s = {0};//1. 打开文件FILE* pf = fopen("test.txt", "r");if (pf == NULL){perror("fopen");return 1;}//2. 读文件//从键盘中获取信息,放入结构体s的各个成员中scanf("%s %d %f", s.name, &(s.age), &(s.score));//从文件中读取信息,存放到s的各个成员中fscanf(pf, "%s %d %f", s.name, &(s.age), &(s.score));//下面两种方法均可将数据打印在屏幕上printf("%s %d %.1f\n", s.name, s.age, s.score);fprintf(stdout, "%s %d %.1f\n", s.name, s.age, s.score);//直接对标准输出流操作//3. 关闭文件fclose(pf);pf = NULL;return 0;
}

经过了上面sprintf函数的介绍,可以知道sscanf函数是一个相反的过程,他会将字符串arr中提取格式化的数据放在程序指向的对应空间去。

代码示例:

//先将结构体s中的格式化数据放入字符串arr中,再从arr中提取格式化的数据放入指定空间中,并显示在屏幕上
struct S
{char name[20];int age;float score;
};int main()
{char arr[100] = { 0 };struct S s = { "wangwu", 23, 66.6f };//临时变量struct S tmp = { 0 };//将s中的各个数据转换成字符串,存放在arr中sprintf(arr, "%s %d %f", s.name, s.age, s.score);//printf("%s\n", arr);//从字符串arr中提取格式化的数据,存放在tmp中sscanf(arr, "%s %d %f", tmp.name, &(tmp.age), &(tmp.score));printf("%s %d %f\n", tmp.name, tmp.age, tmp.score);return 0;
}

总结:

scanf/printf 针对标准输入流/标准输出流的**格式化(在输入和输出数据时需要指定格式)**输入/输出函数

fscanf/fprintf 针对所有输入流/所有输出流的格式化输入/输出函数(可以针对文件也可以针对标准流

sscanf 将格式化的数据写到字符串中,也可以理解为将格式化的数据转换成字符串

sprintf 从字符串中提取格式化的数据,也可以理解为将字符串转换成格式化的数据

5.1.5 fread和fwrite

fwrite函数原型如下:

size_t fwrite(const void *ptr, size_t size, size_t count, FILE *stream);
  • **函数功能:**将数据从内存写入指定的文件流中(以二进制的形式将数据写进文件流中)
  • 参数:
    • ptr:要写入文件的数据的指针
    • size:要写入文件的数据的字节大小
    • count:要写入的元素的数量
    • stream:表示文件流,指定了将数据写入的目标文件
  • **返回值:**写入成功的元素数量
    • 情况一:写入失败,返回值将小于count。如果发生错误或者到达文件末尾,会返回实际写入的元素数。
    • 情况二:写入成功,返回值等于count。

代码示例:

//代码1:将结构体中的数据以二进制的形式写入文件中
struct S
{char name[20];int age;float score;
};int main()
{struct S s = { "cuihua", 25, 88.8f };//以二进制的形式写到文件中//1. 打开文件FILE* pf = fopen("test.txt", "wb");if (pf == NULL){perror("fopen");return 1;}//2.写文件fwrite(&s, sizeof(struct S), 1, pf);//3.关闭文件fclose(pf);pf = NULL;return 0;
}

**补充:**在代码运行成功后,打开“test.txt”文件,会发现里面会出现乱码,这是因为我们的数据是以二进制的形式写入文件,所以使用文本编辑是没有办法看懂的。


fread函数原型如下:

size_t fread(void *ptr, size_t size, size_t count, FILE *stream);

这个函数和上面的fwrite函数十分相似,fwtite函数是把ptr中大小为sizecount个数据写入到stream

所以fread函数是读出steam中的count个大小为size的数据存放到ptr这块空间中。同样fread函数的返回值也是成功读取的元素个数。

代码示例:

//代码2:将已经通过代码2写入文件的二进制数据,在读取出打印在屏幕上
struct S
{char name[20];int age;float score;
};int main()
{struct S s = {0};//读取二进制的信息写到文件中//1. 打开文件FILE* pf = fopen("test.txt", "rb");if (pf == NULL){perror("fopen");return 1;}//2.读文件fread(&s, sizeof(struct S), 1, pf);printf("%s %d %f\n", s.name, s.age, s.score);//打印数据看是否成功读取//3.关闭文件fclose(pf);pf = NULL;return 0;
}

**补充:**代码成功运行之后可以将上面的结构体数据打印在屏幕上,这样也就可以读出代码2写到文件中的二进制数据。

6. 文件的随机读写

在打开文件时,光标始终留在第一位如果这时候进行读写的部分并不是程序员想要操作的部分,如果要实现跳过部分文件内容,不按照文件中数据的排列顺序进行读写就需要就需要将文件光标移动到想要操作的数据前在进行读写,这其中就涉及下面文件的随机读写中的内容。

6.1 fseek

fseek函数原型如下:

int fseek(FILE *stream, long int offset, int origin);
  • **函数功能:**根据文件指针的位置和偏移量来定位文件指针(文件内容的光标)。
  • 参数:
    • stream:需要操作的文件流
    • offset:想要操作的光标位置距origin的偏移量(向右偏为正,向左偏为正)
    • origin:起始位置
      • SEEK_SET:文件的起始位置
      • SEEK_CUR:文件指针当前的位置
      • SEEK_END:文件的末尾
  • 返回值:
    • 返回值为 0:表示成功,文件位置指针已成功移动到新的位置。
    • 返回值为非 0 值:表示失败,文件位置指针未能正确移动,具体失败的原因可以通过 errno 进行检查。

代码示例:

//在文件中操作,在"This is an apple."改为输出“This is a sample.”
/* fseek example */
#include <stdio.h>int main ()
{//1. 打开文件FILE * pFile;pFile = fopen ( "example.txt" , "wb" );//2. 写文件fputs ( "This is an apple." , pFile );//放入数据fseek ( pFile , 9 , SEEK_SET );//移动文件光标fputs ( " sam" , pFile );//操作数据//3. 关闭文件fclose ( pFile );pFile = NULL;return 0;
}

6.2 ftell

ftell函数原型如下:

long int ftell(FILE *stream);
  • **函数功能:**返回文件指针相对于起始位置的偏移量。
  • 参数:
    • stream:需要操作的文件流
  • 返回值:
    • 返回值为 -1:表示发生错误。
    • 返回值为非 0 值:正常情况,返回值是文件当前位置相对于文件开头的字节数,即输出文件指针相较于文件起始位置的偏移量。

例子:

//算出文件中的的数据有多少个字节
/* ftell example : getting size of a file */
#include <stdio.h>int main ()
{//1. 打开文件FILE * pFile;long size;pFile = fopen ("myfile.txt","rb");if (pFile==NULL) perror ("Error opening file");else{//2. 读文件fseek (pFile, 0, SEEK_END); //直接定位到文件末尾size=ftell (pFile);//3. 关闭文件fclose (pFile);pFile = NULL;printf ("Size of myfile.txt: %ld bytes.\n",size);}return 0;
}

6.3 rewind

rewind函数原型如下:

void rewind(FILE *stream);
  • **函数功能:**让文件指针的位置重新定位,回到文件的起始位置。
  • 参数:
    • stream:需要操作的文件流
  • **返回值:**无

例子:

//将A~Z的二进制形式显示在屏幕上
/* rewind example */
#include <stdio.h>int main ()
{int n;FILE * pFile;char buffer [27];//1. 打开文件pFile = fopen ("myfile.txt","w+");//2. 操作文件for ( n='A' ; n<='Z' ; n++){fputc ( n, pFile);}rewind (pFile);fread (buffer,1,26,pFile);//将写入文件的A~Z,以二进制的形式读出并存放到数组buffer中去fclose (pFile);pFile = NULL;buffer[26]='\0';printf(buffer);return 0;
}

7. 文件读取结束的判定

7.1 被错误使用的 feof

牢记:在文件读取过程中,不能用 feof 函数的返回值直接来判断文件是否结束。

feof 的作用是:当文件读取结束时,判断是读取结束的原因是否是:遇到文件尾结束。

feof函数原型如下:

int feof(FILE *stream);
  • 返回值:
    • 返回值为非零值:表示文件已经到达末尾。具体来说,如果文件流的当前位置已经到达文件的末尾,feof() 会返回一个非零值(通常是1)。
    • 返回值为0:表示文件并未到达末尾。也就是说,文件指针并不在文件的末尾,仍然可以继续读取数据。

例子:

  1. 利用feof函数检测文本文件读取是否结束,可以判断返回值是否为EOFfgetc),或者NULLfgets

例如:

  • fgetc判断是否为EOF(表示出现错误)
  • fgets判断返回值是否为NULL(表示出现错误)
  1. 利用feof函数检测二进制文件的读取结束判断,判断返回值是否小于实际要读的个数。

例如:

  • fread判断返回值是否小于实际要读的个数(出现读取错误时会出现这种情况)。

文本文件的例子:

//读取文件"test.txt"中的内容并显示在屏幕上,并附上最后读取结束的原因
#include <stdio.h>
#include <stdlib.h>int main(void)
{int c; // 注意:int,⾮char,要求处理EOF //1. 打开文件FILE* fp = fopen("test.txt", "r");if(!fp) {perror("File opening failed");return EXIT_FAILURE;}//2. 操作文件//fgetc 当读取失败的时候或者遇到⽂件结束的时候,都会返回EOF while ((c = fgetc(fp)) != EOF) // 标准C I/O读取⽂件循环 读取文件中的内容 { putchar(c);}//判断是什么原因结束的 if (ferror(fp))	//检测是否是文件IO的错误{puts("I/O error when reading");}else if (feof(fp))	//检测是否读取到文件末尾{puts("End of file reached successfully");  }//3. 关闭文件fclose(fp);fp = NULL;
}

在第22行引出了一个新的函数ferror函数,函数原型如下:

int ferror(FILE *stream);
  • **函数功能:**用于检查文件流是否发生了错误。
  • 参数:
    • stream:要操作的文件流
  • 返回值:
    • 返回值为非零值:表示在文件操作过程中发生了错误。
    • 返回值为0:表示没有发生错误,文件流操作正常。

根据以上对ferror函数的介绍可以得知,在上述代码进行文件错误类型的判读中可以知道,当代码运行起来的时候,可以根据在屏幕上返回的错误原因得出读取文件结束的原因是什么。

如果是文件IO出现错误则ferror函数会返回一个非零值,此时在屏幕上会显示错误原因为**“I/O error when reading"如果是已经读取到文件末尾则feof函数会返回一个非零值,此时会在屏幕上显示错误原因为"End of file reached successfully”**。

二进制文件的例子:

//将1.0 2.0 3.0 4.0 5.0以二进制形式存储到文件"test.bin"中去,并再以二进制的形式读取这个文件,如果正确读取将读取的内容打印到屏幕上,如果读取错误将错误原因打印到屏幕上
#include <stdio.h>enum { SIZE = 5 };
int main(void)
{double a[SIZE] = {1.,2.,3.,4.,5.};//1. = 1.0//1. 打开文件FILE *fp = fopen("test.bin", "wb"); // 必须⽤⼆进制模式 //2. 操作文件fwrite(a, sizeof *a, SIZE, fp); //将来自于a这个空间的大小为(sizeof *a)的SIZE个数据写入到fp指向的文件流中去//3. 关闭文件fclose(fp);fp = NULL;double b[SIZE];//1. 打开文件fp = fopen("test.bin","rb");//2. 操作文件size_t ret_code = fread(b, sizeof *b, SIZE, fp); // 将fp指向的文件流中的大小为(sizeof *a)的SIZE个数据读取到空间b中if(ret_code == SIZE) //正确读取{puts("Array read successfully, contents: ");for(int n = 0; n < SIZE; ++n) printf("%f ", b[n]);//打印正常读取的内容putchar('\n');} else //读取错误{ // error handlingif (feof(fp))//遇到文件末尾printf("Error reading test.bin: unexpected end of file\n");else if (ferror(fp)) //文件IO的错误{perror("Error reading test.bin");}}//3. 关闭文件fclose(fp);fp = NULL;
}

8. 文件缓冲区

ANSI C标准采用**“缓冲文件系统”处理的数据文件的,所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块“文件缓冲区”**。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。

缓冲区的大小根据C编译系统决定的。

**文件缓冲区存在的意义:**因为每次程序数据区和硬盘之间进行数据交换都需要调用操作系统的接口,如果每交换一个数据就调用一次这样频繁的调用会增加操作系统的负担,并且使得操作系统没有办法处理其他程序。因为引用了数据缓冲区,假设每交换五个数据再调用一次操作系统的接口进行数据传输,这样就可以使得操作系统有时间去处理其他程序,增加操作系统的效率。

在这里插入图片描述

#include <stdio.h>
#include <windows.h>//VS2022 WIN11环境测试 int main()
{FILE*pf = fopen("test.txt", "w");fputs("abcdef", pf);//先将代码放在输出缓冲区 printf("睡眠10秒-已经写数据了,打开test.txt⽂件,发现⽂件没有内容\n");Sleep(10000);//单位为ms,即10sprintf("刷新缓冲区\n");fflush(pf);//刷新缓冲区时,才将输出缓冲区的数据写到⽂件(磁盘) //注:fflush 在⾼版本的VS上不能使⽤了 printf("再睡眠10秒-此时,再次打开test.txt⽂件,⽂件有内容了\n");Sleep(10000);fclose(pf);//注:fclose在关闭⽂件的时候,也会刷新缓冲区 pf = NULL;return 0;
}

这里可以得出一个结论:

因为有缓冲区的存在,C语言在操作文件的时候,需要做刷新缓冲区或者文件操作结束的时候关闭文件。如果不做,可能到导致读写文件的问题。

9. 文件操作的练习

9.1 两个文件之间的拷贝

通过文件操作将文件“test1.txt”的内容拷贝到“test2.txt”中去。

#include <stdio.h>int main()
{//1. 打开文件FILE* pRead = fopen("test1.txt","r");if(pRead == NULL){perror("fopen\n");return 1;}FILE* pWrite = fopen("test2.txt","w"){perror("fopen\n");fclose(pRead);return 1;}//2. 操作文件while((ch = fgetc(pRead)) != EOF)//读取文本{fputc(ch,pWrite);//拷贝文本}//3. 关闭文件fclose(pRead);pRead = NULL;fclose(pWrite)pWrite = NULL;return 0;
}

相关文章:

C语言--文件操作

第16讲&#xff1a;文件操作 1. 为什么使用文件&#xff1f; 如果没有文件&#xff0c;我们写的程序的数据是存储在电脑的内存中&#xff0c;如果程序退出&#xff0c;内存回收&#xff0c;数据就丢失了&#xff0c;等再次运行程序&#xff0c;是看不到上次程序的数据的&…...

网络安全内参

在我的IT职业生涯的早期&#xff0c;Sun Microsystems被认为是一个计算机梦想家。Sun很早就创造了一个有趣的公司口号:“The network is the computer.”这是什么意思&#xff1f;这意味着IT基础设施以松散耦合的体系结构连接在一起&#xff0c;通过以太网电缆和TCP/IP协议等网…...

C++大整数类的设计与实现

1. 简介 我们知道现代的计算机大多数都是64位的&#xff0c;因此能处理最大整数为 2 64 − 1 2^{64}-1 264−1。那如果是超过了这个数怎么办呢&#xff0c;那就需要我们自己手动模拟数的加减乘除了。 2. 思路 我们可以用一个数组来存储大数&#xff0c;数组中的每一个位置表…...

在 macOS 系统上安装 kubectl

在 macOS 系统上安装 kubectl 官网&#xff1a;https://kubernetes.io/zh-cn/docs/tasks/tools/install-kubectl-macos/ 用 Homebrew 在 macOS 系统上安装 如果你是 macOS 系统&#xff0c;且用的是 Homebrew 包管理工具&#xff0c; 则可以用 Homebrew 安装 kubectl。 运行…...

【人工智能】蓝耘智算平台盛大发布DeepSeek满血版:开创AI推理体验新纪元

&#x1f4dd;个人主页&#x1f339;&#xff1a;Eternity._ &#x1f339;&#x1f339;期待您的关注 &#x1f339;&#x1f339; ❀ 蓝耘智算平台 蓝耘智算平台核心技术与突破元生代推理引擎快速入门&#xff1a;三步调用大模型接口&#xff0c;OpenAI SDK无缝兼容实战用例文…...

构建数据治理闭环:DAMA视角下的全流程实践与价值变现

随着数字经济的迅速发展&#xff0c;数据已成为企业核心资产&#xff0c;高效的数据治理体系正变得至关重要。本文基于DAMA理论&#xff0c;从数据资产入表、分类分级、确权登记到元数据管理、数据质量监控&#xff0c;再到数据集成、互操作及主数据管理&#xff0c;全流程构建…...

《深度剖析:AI与姿态估计技术在元宇宙VR交互中的应用困境》

在元宇宙的宏大版图里&#xff0c;虚拟现实&#xff08;VR&#xff09;交互是构建沉浸式体验的关键支柱&#xff0c;而人工智能&#xff08;AI&#xff09;与姿态估计技术的融合&#xff0c;本应成为提升交互体验的强大引擎。但在实际应用中&#xff0c;它们面临着诸多复杂且棘…...

【Python LeetCode】面试经典 150 题

数组 / 字符串快慢指针&#xff08;双指针&#xff09;总结88. 合并两个有序数组27. 移除元素26. 删除有序数组中的重复项80. 删除有序数组中的重复项 II Boyer-Moore 投票算法169. 多数元素扩展&#xff1a;寻找 n/3 多数元素 翻转法189. 轮转数组 贪心121. 买卖股票的最佳时机…...

2011-2019年各省乡镇综合文化站机构数数据

2011-2019年各省乡镇综合文化站机构数数据 1、时间&#xff1a;2011-2019年 2、来源&#xff1a;国家统计局、统计年鉴 3、指标&#xff1a;行政区划代码、地区、年份、乡镇综合文化站机构数 4、范围&#xff1a;31省 5、指标解释&#xff1a;乡镇综合文化站是中国基层文化…...

LeetCode 热题100 226. 翻转二叉树

LeetCode 热题100 | 226. 翻转二叉树 大家好&#xff0c;今天我们来解决一道经典的算法题——翻转二叉树。这道题在 LeetCode 上被标记为简单难度&#xff0c;要求我们翻转一棵二叉树&#xff0c;并返回其根节点。下面我将详细讲解解题思路&#xff0c;并附上 Python 代码实现…...

mysql 拼接多行合并为一行

如图所示&#xff0c;在variety相同的前提下拼接rating为ratingList&#xff0c;year_term为yearTermList sql如下&#xff1a; SELECT variety,GROUP_CONCAT(rating ORDER BY rating SEPARATOR ,) AS ratingList,GROUP_CONCAT(year_term ORDER BY year_term SEPARATOR…...

【Java项目】基于Spring Boot的论坛管理系统

【Java项目】基于Spring Boot的论坛管理系统 技术简介&#xff1a;采用Java技术、Spring Boot框架、MySQL数据库等实现。 系统简介&#xff1a;论坛管理系统是一个基于Web的在线平台&#xff0c;主要分为前台和后台两大功能模块。前台功能模块包括&#xff08;1&#xff09;首…...

unity学习54:图片+精灵+遮罩mask,旧版文本 text 和新的TMP文本

目录 1 图片 image 1.1 如果直接导入image 1.2 图片 image 和精灵 sprite 1.2.1 继续修改上面的格式 texture type 是default 1.2.2 再次关联到UI的 image 物体上就可以了 1.3 图片和遮罩 mask 1.3.1 创建1个父物体和1个子物体&#xff0c;分别都是image 1.3.2 如果父…...

2024年国赛高教杯数学建模D题反潜航空深弹命中概率问题解题全过程文档及程序

2024年国赛高教杯数学建模 D题 反潜航空深弹命中概率问题 原题再现 应用深水炸弹&#xff08;简称深弹&#xff09;反潜&#xff0c;曾是二战时期反潜的重要手段&#xff0c;而随着现代军事技术的发展&#xff0c;鱼雷已成为现代反潜作战的主要武器。但是&#xff0c;在海峡或…...

什么是数字人

什么是数字人 Ultralight-Digital-Human 是一个能在移动设备上实时运行的数字人模型仓库,可能是第一个开源的如此轻量级的数字人模型。 主要特点 轻量级:能够在移动设备上实时运行。开源:代码和模型公开,方便开发者使用和改进。文件结构 根目录: README.md:项目的说明文…...

15.5 基于 RetrievalQA 的销售话术增强系统实战:构建智能销售大脑

基于 RetrievalQA 的销售话术增强系统实战:构建智能销售大脑 关键词:RetrievalQA 应用实战、销售知识增强、语义检索优化、上下文感知问答、多源知识融合 1. RetrievalQA 技术原理与销售场景适配 1.1 RetrievalQA 核心工作机制 #mermaid-svg-VL2yIusgl4oprXUr {font-family…...

软件供应链安全工具链研究系列—RASP自适应威胁免疫平台(下篇)

在“软件供应链安全工具链研究系列—RASP自适应威胁免疫平台-上篇”中我们提到了RASP工具的基本能力、原理以及工具的应用场景&#xff0c;了解到了RASP工具在各场景下发挥的价值。那么在当今高强度攻防对抗的大场景下&#xff0c;RASP作为最后一道防线&#xff0c;不论是从高危…...

WordPress网站502错误全面排查与解决指南

502 Bad Gateway错误是WordPress站长最常遇到的服务器问题之一,它意味着服务器作为网关或代理时,未能从上游服务器获取有效响应。针对WP可能出现的502问题,本文提供一些基础到进阶的解决方案供大家参考:) 一、502错误的本质和核心诱因 502错误属于HTTP状态码中的5xx系列,…...

PCL源码分析:曲面法向量采样

文章目录 一、简介二、源码分析三、实现效果参考资料一、简介 曲面法向量点云采样,整个过程如下所述: 1、空间划分:使用递归方法将点云划分为更小的区域, 每次划分选择一个维度(X、Y 或 Z),将点云分为两部分,直到划分区域内的点少于我们指定的数量,开始进行区域随机采…...

HTTP 动态报错码的原因和解决方法

目录 1xx&#xff08;信息性状态码&#xff09; 2xx&#xff08;成功状态码&#xff09; 3xx&#xff08;重定向状态码&#xff09; 4xx&#xff08;客户端错误状态码&#xff09; 5xx&#xff08;服务器错误状态码&#xff09; 参考文章 以下是 HTTP 动态报错码的常见原…...

Java 8 Stream API 入门到实践详解

一、告别 for 循环&#xff01; 传统痛点&#xff1a; Java 8 之前&#xff0c;集合操作离不开冗长的 for 循环和匿名类。例如&#xff0c;过滤列表中的偶数&#xff1a; List<Integer> list Arrays.asList(1, 2, 3, 4, 5); List<Integer> evens new ArrayList…...

Redis相关知识总结(缓存雪崩,缓存穿透,缓存击穿,Redis实现分布式锁,如何保持数据库和缓存一致)

文章目录 1.什么是Redis&#xff1f;2.为什么要使用redis作为mysql的缓存&#xff1f;3.什么是缓存雪崩、缓存穿透、缓存击穿&#xff1f;3.1缓存雪崩3.1.1 大量缓存同时过期3.1.2 Redis宕机 3.2 缓存击穿3.3 缓存穿透3.4 总结 4. 数据库和缓存如何保持一致性5. Redis实现分布式…...

华为OD机试-食堂供餐-二分法

import java.util.Arrays; import java.util.Scanner;public class DemoTest3 {public static void main(String[] args) {Scanner in new Scanner(System.in);// 注意 hasNext 和 hasNextLine 的区别while (in.hasNextLine()) { // 注意 while 处理多个 caseint a in.nextIn…...

Psychopy音频的使用

Psychopy音频的使用 本文主要解决以下问题&#xff1a; 指定音频引擎与设备&#xff1b;播放音频文件 本文所使用的环境&#xff1a; Python3.10 numpy2.2.6 psychopy2025.1.1 psychtoolbox3.0.19.14 一、音频配置 Psychopy文档链接为Sound - for audio playback — Psy…...

css的定位(position)详解:相对定位 绝对定位 固定定位

在 CSS 中&#xff0c;元素的定位通过 position 属性控制&#xff0c;共有 5 种定位模式&#xff1a;static&#xff08;静态定位&#xff09;、relative&#xff08;相对定位&#xff09;、absolute&#xff08;绝对定位&#xff09;、fixed&#xff08;固定定位&#xff09;和…...

保姆级教程:在无网络无显卡的Windows电脑的vscode本地部署deepseek

文章目录 1 前言2 部署流程2.1 准备工作2.2 Ollama2.2.1 使用有网络的电脑下载Ollama2.2.2 安装Ollama&#xff08;有网络的电脑&#xff09;2.2.3 安装Ollama&#xff08;无网络的电脑&#xff09;2.2.4 安装验证2.2.5 修改大模型安装位置2.2.6 下载Deepseek模型 2.3 将deepse…...

视觉slam十四讲实践部分记录——ch2、ch3

ch2 一、使用g++编译.cpp为可执行文件并运行(P30) g++ helloSLAM.cpp ./a.out运行 二、使用cmake编译 mkdir build cd build cmake .. makeCMakeCache.txt 文件仍然指向旧的目录。这表明在源代码目录中可能还存在旧的 CMakeCache.txt 文件,或者在构建过程中仍然引用了旧的路…...

Go 语言并发编程基础:无缓冲与有缓冲通道

在上一章节中&#xff0c;我们了解了 Channel 的基本用法。本章将重点分析 Go 中通道的两种类型 —— 无缓冲通道与有缓冲通道&#xff0c;它们在并发编程中各具特点和应用场景。 一、通道的基本分类 类型定义形式特点无缓冲通道make(chan T)发送和接收都必须准备好&#xff0…...

【电力电子】基于STM32F103C8T6单片机双极性SPWM逆变(硬件篇)

本项目是基于 STM32F103C8T6 微控制器的 SPWM(正弦脉宽调制)电源模块,能够生成可调频率和幅值的正弦波交流电源输出。该项目适用于逆变器、UPS电源、变频器等应用场景。 供电电源 输入电压采集 上图为本设计的电源电路,图中 D1 为二极管, 其目的是防止正负极电源反接, …...

LLMs 系列实操科普(1)

写在前面&#xff1a; 本期内容我们继续 Andrej Karpathy 的《How I use LLMs》讲座内容&#xff0c;原视频时长 ~130 分钟&#xff0c;以实操演示主流的一些 LLMs 的使用&#xff0c;由于涉及到实操&#xff0c;实际上并不适合以文字整理&#xff0c;但还是决定尽量整理一份笔…...