前言:
为什么要学习文件操作呢?想要知道这个问题,我们就需要先了解什么是数据的可持久化。
那么什么是数据的可持久化呢?数据的可持久化就是把内存中的数据对象永久的保存在电脑的磁盘文件中,将程序数据在持久状态和瞬时状态相互转换的机制。
而文件操作就可以让我们达到这一目的。
1.什么是文件
磁盘上的文件都是文件。
在程序设计中,我们将文件分为两种:程序文件、数据文件(从文件功能角度来分类)。
1.1程序文件
包括源程序文件(后缀为.c),目标文件(windows环境后缀为.obj),可执行程序(windows环境 后缀为.exe)。
像我们写代码时创建的test.c或者test.cpp文件就是程序文件。
1.2 数据文件
数据文件存放程序在运行时需要读取的数据。
列如通讯录中的各个对象的数据,在使用通讯录程序时我们需要先从数据文件中读取数据。
接下来我们主要讨论的是数据文件。
在我们日常写代码刷题,处理数据的输入输出都是终端作为对象,即从终端的键盘输入数据,运行结果显示到显示器上。
就像这个:
其实更多时候我们会把信息输出到磁盘上,当需要的时候再从磁盘上把数据读取到内存中使用,这里处理 的就是磁盘上文件。
1.3 文件名
一个文件要有一个唯一的文件标识,以便用户识别和引用。
文件名包含3部分:文件路径+文件名主干+文件后缀
比如:
c:\code\test.txt
这个路径的意思就是c盘下的code文件夹里面的test.txt文件
为了方便查找,文件标识常被称为文件名。 而且在同一目录下的文件名必须不一样。
2.文件的打开与关闭
2.1文件指针
文件指针其实就是文件类型的指针。
在我们使用一个文件时,会在内存开辟一块相应的文件信息区,用来存放文件的相关信息(如文件的名 字,文件状态及文件当前的位置等)。这些信息是保存在一个结构体变量中的。该结构体类型是由系统声明的,取名为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;
注意,不同的编译器FILE类型包含的内容不完全相同,但是大同小异。
每当打开一个文件的时候,系统会根据文件的情况自动创建一个FILE结构的变量,并填充其中的信息, 使用者不必关心细节。
一般都是通过一个FILE的指针来维护这个FILE结构的变量,这样使用起来更加方便。
下面我们可以创建一个FILE*的指针变量:
FILE* pf;//文件指针变量
2.2文件的打开与关闭
要想将数据写入文件,或者是将文件中的数据读出来,都需要先打开目标文件。就像我们如果想喝牛奶,那肯定是先把瓶盖拧开。同样的,在完成一些列操作后,我们需要把文件关闭,跟喝完牛奶要盖上瓶盖一样的道理。
在打开文件的时候,会返回一个FILE类型的指针来指向该文件,我们就通过这个指针来对文件进行数据的读取和写入。
ANSIC 规定使用fopen函数来打开文件,fclose来关闭文件。
//打开文件
FILE * fopen ( const char * filename, const char * mode );
//关闭文件
int fclose ( FILE * stream ) ;
当我们使用fopen来打开文件的时候,需要传入的参数有两个,一个是文件路径filename,还有一个是打开文件的方式mode.
文件路径怎么书写?
在C语言中,文件路径的书写是根据操作系统的不同而有所差异。下面是一些常用操作系统的文件路径书写方式示例:
- 绝对路径:C:\folder\file.c
- 相对路径:folder\file.c 或者 …\folder\file.c (… 表示上一级目录)
绝对路径举例:
c:\code\test.txt
相对路径举例:
..\\code\\test.txt
这里为什么要用“\\”而不是“\”呢?这就是涉及到转移字符的问题了,这里就不多说了。
有哪些打开方式呢?
文件使用方式 | 含义 | 如果指定文件不存在 |
---|---|---|
“r”(只读) | 为了输入数据,打开一个已经存在的文本文件 | 出错 |
“w”(只写) | 为了输出数据,打开一个文本文件 | 建立一个新的文件 |
“a”(追加) | 向文本文件尾添加数据 | 建立一个新的文件 |
“rb”(只读) | 为了输入数据,打开一个二进制文件 | 出错 |
“wb”(只写) | 为了输出数据,打开一个二进制文件 | 建立一个新的文件 |
“ab”(追加) | 向一个二进制文件尾添加数据 | 出错 |
“r+”(读写) | 为了读和写,打开一个文本文件 | 出错 |
“w+”(读写) | 为了读和写,建议一个新的文件 | 建立一个新的文件 |
“a+”(读写) | 打开一个文件,在文件尾进行读写 | 建立一个新的文件 |
“rb+”(读写) | 为了读和写打开一个二进制文件 | 出错 |
“wb+”(读写) | 为了读和写,新建一个新的二进制文件 | 建立一个新的文件 |
“ab+”(读写) | 打开一个二进制文件,在文件尾进行读和写 | 建立一个新的文件 |
实例:
#include <stdio.h>
int main ()
{FILE * pFile;//打开文件pFile = fopen ("myfile.txt","w");//文件操作if (pFile!=NULL){fputs ("fopen example",pFile);//关闭文件fclose (pFile);}return 0;
}
3.文件的顺序读写
功能 | 函数名 | 适用于 |
---|---|---|
字符输入函数 | fgetc | 所有输入流 |
字符输出函数 | fputc | 所有输出流 |
文本行输入函数 | fgets | 所有输入流 |
文本行输出函数 | fputs | 所有输出流 |
格式化输入函数 | fscanf | 所有输入流 |
格式化输出函数 | fprintf | 所有输出流 |
二进制输入函数 | fread | 文件 |
二进制输出函数 | fwrite | 文件 |
3.1scanf/fscanf/sscanf的区别:
3.1.1 scanf
从标准输入(即键盘)读取输入数据。它的函数原型为int scanf(const char *format, ...)
,其中format
是格式字符串,用于指定输入数据的格式。scanf
根据格式字符串解析输入数据,并将解析结果存储到对应的参数中。
3.1.2 fscanf
从文件中读取输入数据。它的函数原型为int fscanf(FILE *stream, const char *format, ...)
,其中stream
是指向文件的指针,format
是格式字符串。fscanf
根据格式字符串从文件中解析输入数据,并将解析结果存储到对应的参数中。
3.1.3 sscanf
从字符串中读取输入数据。它的函数原型为int sscanf(const char *str, const char *format, ...)
,其中str
是输入字符串,format
是格式字符串。sscanf
根据格式字符串从字符串中解析输入数据,并将解析结果存储到对应的参数中。
区别:
这些函数在功能上是类似的,但是适用的输入源不同。scanf
适用于从标准输入(键盘)读取数据,fscanf
适用于从文件读取数据,sscanf
适用于从字符串读取数据。
需要注意的是,这些函数都可以返回成功匹配并读取的参数个数,用于判断读取是否成功。返回值为EOF(-1)表示读取失败。此外,它们的格式字符串的语法是相同的,都可以使用格式控制符来指定输入数据的类型和格式。
printf/fprintf/sprintf的区别跟上面的一样,只不过是输出数据。
4.文件的随机读写
4.1 fseek
根据文件指针的位置和偏移量来定位文件指针.
int fseek ( FILE * stream, long int offset, int origin );
举例:
#include <stdio.h>
int main()
{FILE* pFile;pFile = fopen("example.txt", "wb");fputs("This is an apple.", pFile);fseek(pFile, 9, SEEK_SET);fputs(" sam", pFile);fclose(pFile);return 0;
}
打开example.txt文件
我们发现“sam"被插入到了字符串"This is an apple."中第九个位置。
具体是怎么实现的呢?
来看这一行代码
fseek(pFile, 9, SEEK_SET);
pFile
是指向文件的指针;9
是offset
参数,表示要移动的字节数或记录数;SEEK_SET
是origin
参数,用于指定相对位置。SEEK_SET
表示从文件的开头开始计算偏移量。
我们通过这一行代码将文件指针pFile的位置向后移动了9个位置,所以我们在接下来的写入操作时,是从第9个位置开始写入的。
4.2 ftell
返回文件指针相对于起始位置的偏移量
long int ftell ( FILE * stream );
举例:
int main()
{FILE* pFile;long size;pFile = fopen("example.txt", "rb");if (pFile == NULL) perror("Error opening file");else{fseek(pFile, 0, SEEK_END); // non-portablesize = ftell(pFile);fclose(pFile);printf("Size of example.txt: %ld bytes.\n", size);}return 0;
}
这里我们先用fseek将文件指针移动到数据字节的最后一个位置,这个时候ftell返回的就是起始位置到末位置的距离。
4.3 rewind
让文件指针的位置回到文件的位置
void rewind ( FILE * stream );
举例:
int main() {FILE* pf = fopen("example.txt", "w+");//读和写文件char arr[27] = { 0 };for (char i = 'a'; i <= 'z'; i++) {fputc(i, pf);}//将26个字母写入到文件中//此时文件指针的位置指向最后一个位置//输出此时文件指针偏移量int x = ftell(pf);printf("此时文件指针跟起始位置的偏移量为%d\n", x);rewind(pf);//将文件指针的位置移到起始位置fread(arr, 1, 26, pf);//将文件里的数据读入到arr中for (int i = 0; i < 27; i++) {printf("%c ", arr[i]);}fclose(pf);pf = NULL;return 0;
}
我们可以看到,通过frewind将文件指针移动到起始位置,我们才能从头开始将26个字母读入到字符数组arr中。
5.文本文件和二进制文件
5.1二进制文件
我们知道,在内存中所有数据都是以二进制的形式储存的。
如果不加任何转换就将其输入到文件中,那这种文件就是二进制文件,而这种文件里的数据我们通常是看不懂的。
举例:
int main() {FILE* pf = fopen("example.txt", "wb+");//二进制文件的读和写int a=10000;fwrite(&a, 4, 1, pf);fclose(pf);pf = NULL;return 0;
}
是不是看不懂?
用二进制编译器打开一看
10 27 00 00这是表示的是啥意思?
其实这就是二进制中的10000,只不过是用16进制表示出来了
5.2 文本文件?
求在外存上以ASCII码的形式存储,则需要在存储前转换。以ASCII字符的形式存储的文件就是文 本文件。
用以上例子在文本文件中又是怎么表示的呢?
10000在文本文件中就是五个字符排列在一起的,所以10000的ASCII码的形式存储形式是
6.文件读取结束的判定
6.1被错误使用的feof
feof函数是C语言中的一个库函数,用于检测文件流的结束标志。它的作用是判断文件是否已经达到文件末尾。当文件读取到末尾时,feof函数返回非零值(真),否则返回零值(假)。
注意:
在文件读取过程中,不能用feof函数的返回值直接用来判断文件的是否结束。 而是应用于当文件读取结束的时候,判断是读取失败结束,还是遇到文件尾结束。
1. 文本文件读取是否结束,判断返回值是否为EOF(fgetc),或者NULL(fgets)。
2. 二进制文件的读取结束判断,判断返回值是否小于实际要读的个数。
正确使用:
1.文本文件中:
#include <stdio.h>
#include <stdlib.h>
int main(void)
{int c; // 注意:int,非char,要求处理EOFFILE* fp = fopen("test.txt", "r");if(!fp) {perror("File opening failed");return EXIT_FAILURE;}//fgetc 当读取失败的时候或者遇到文件结束的时候,都会返回EOFwhile ((c = fgetc(fp)) != EOF) // 标准C I/O读取文件循环{putchar(c);}//判断是什么原因结束的if (ferror(fp))puts("I/O error when reading");else if (feof(fp))puts("End of file reached successfully");fclose(fp);
}
2.二进制文件中:
#include <stdio.h>
enum { SIZE = 5 };
int main(void)
{double a[SIZE] = {1.,2.,3.,4.,5.};FILE *fp = fopen("test.bin", "wb"); // 必须用二进制模式fwrite(a, sizeof *a, SIZE, fp); // 写 double 的数组fclose(fp);double b[SIZE];fp = fopen("test.bin","rb");size_t ret_code = fread(b, sizeof *b, SIZE, fp); // 读 double 的数组if(ret_code == SIZE) {//判断成功读取的的数据次数是否等于SIZEputs("Array read successfully, contents: ");for(int n = 0; n < SIZE; ++n) printf("%f ", b[n]);putchar('\n');} else { // 不等于说明读取失败了,下面排查失败原因if (feof(fp))//读取到了末尾printf("Error reading test.bin: unexpected end of file\n");else if (ferror(fp)) {//没有读取到末尾perror("Error reading test.bin");}}fclose(fp);return 0;
}
7.文件缓冲区
缓冲区概念:
ANSIC 标准采用“缓冲文件系统”处理的数据文件的,所谓缓冲文件系统是指系统自动地在内存中为程序 中每一个正在使用的文件开辟一块“文件缓冲区”。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓 冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的大小根 据C编译系统决定的。
同时,缓冲区根据其对应的是输入设备还是输出设备,分为输入缓冲区和输出缓冲区。
为什么要引入缓冲区?
比如我们从磁盘里取信息,我们先把读出的数据放在缓冲区,计算机再直接从缓冲区中取数据,等缓冲区的数据取完后再去磁盘中读取,这样就可以减少磁盘的读写次数,再加上计算机对缓冲区的操作大大快于对磁盘的操作,故应用缓冲区可大大提高计算机的运行速度。
总而言之,缓冲区使得低速的输入输出设备和高速的CPU能够协调工作,避免低速的输入输出设备占用CPU,解放出CPU,使其能够高效率工作。
这里再介绍一个刷新缓冲区的函数fflush:
int fflush ( FILE * stream );
其中,stream 是要刷新的流的指针。如果 stream 为 NULL,则刷新所有流的缓冲区。
8.可持久化数据的具体项目例子
这里就给大家看一看我之前写的通讯录:
c语言小课设--通讯录(动态内存管理+可持久化数据)-CSDN博客该项目实现一个通讯录功能,除了能根据具体需求扩大空间之外,也实现了最基本基本的增删查改等功能,并在退出通讯录时销毁创造的空间,从而不造成内存泄露。另外,这个项目由三部分组成,函数功能的实现在Contact.c源文件中,各种头文件、函数等声明则由文件Contact.h来实现,最后测试在源文件test.c文件中进行。https://blog.csdn.net/qq_62987647/article/details/133466779?csdn_share_tail=%7B%22type%22%3A%22blog%22%2C%22rType%22%3A%22article%22%2C%22rId%22%3A%22133466779%22%2C%22source%22%3A%22qq_62987647%22%7D
9.总结
文件操作的学习可以让我们持久化数据,同时也理解了文件数据是怎么输入输出的。
有啥用呢?
用处大多了,比如说我们在用c语言写一个程序的时候,我们就可以将程序中的数据存放在文件中,以便于我们下次打开程序还能读取到,这也就更加符合我们的实际需求。
其实呢,当你学到文件这一块内容时,你对c语言的认知基本上有了一个新的高度,但是并不意味着是c语言学习的终点,学海无涯,我们当以谦卑的心态学习。
事已至此,好好学习,给我点赞,加油!