引言
关于文件,想必大家或多或少都会有些了解,文件可以帮我们储存数据,不同格式的文件可以储存不同类型的数据,也可以将文件中的数据用不同的方式打开。电脑中的文件,是放在硬盘上的。在我们编写代码并运行的时候,如果没有文件,我们写的程序数据只会在电脑内存中,一旦我们退出程序,内存便会回收,数据会丢失,为了将数据持久化,我们可能需要使用文件。
关于文件
在我们的程序设计中,分为两种文件,一个是程序文件(用来存放运行代码,如:test.c,test.obj),另一个是数据文件(用来存放程序运行时读写的数据)。
其中,数据文件还可以分为:
1.文本文件:以ASCII字符形式存储
2.二进制文件:以二进制形式存储
每一个文件都有一个唯一表示而标识分为三部分:
文件路径 + 文件名主干 + 问价后缀
eg:C:\code\test.exe
关于文件操作
这里先给大家举个例子吧
#include<stdio.h> int main() { int a = 10000; FILE* pf = fopen("text.txt", "wb");//以二进制只写方式打开文件text.txt fwrite(&a, 4, 1, pf);//以二进制形式写入a fclose(pf);//关闭pf指向文件 pf = NULL;//指针制空,避免野指针 return 0; }
根据以上这一份代码,目前不需要完全看懂,这时你只需要了解到操作文件的一个基本的流程
操作文件流程:
1.打开文件
2.读/写文件
3.关闭文件
流
关于文件的操作,还引入了流这个概念。那么流到底是什么呢?其实你可以从字面上去理解它,就是一条小河,不过里面流动的不是水,而是数据,流作为一个连接程序运行和外部设备的一个中间媒介,起着传输数据的作用。有了流,程序在运行中才能读取键盘输入或文件中保存的数据;有了流,才能将程序运行时产生的数据打印到屏幕上或写到文件中。
将这一概念抽象成图是这样的:
流这个中间媒介是抽象出来的,具体的底层实现我们可以先不了解,但是我们需要了解如何正确打开和使用流。
我相信能看到文件操作读者以及对C语言有了相当的了解,可能你会问,为什么我们在之前写代码的时候直接就用scanf和printf函数了,没有见到打开和关闭流的操作啊?
那是因为,这个工作C语言系统已经事先帮我们做好了
在C语言程序中,默认打开三个流:
1.stdin:标准输入流
2.stdout:标准输出流
3.stderr:标准错误流
这几个流的类型都是FILE*(文件指针)
这又说到了文件指针
文件指针(文件类型指针)
文件在内存中有一个文件信息区,用来存放文件信息,这些信息被放在一个结构体变量中,这个结构体就是FILE
在C语言的Visual Studio 2013中,FILE是一个系统定义的结构体类型,用于表示一个文件的相关信息。在stdio.h头文件中,FILE结构体被定义为:
typedef struct _iobuf { char* ptr; int _cnt; char* _base; int _flag; int _file; int _charbuf; int _bufsiz; char* _tmpfname; } FILE;
在这个结构体中,包含了文件名、文件状态和文件当前位置等信息。通过使用指向FILE结构体的指针,可以更加方便地对文件进行操作。
这个结构体格式也不需要大家背下来,只是便于大家理解FILE,不同C编译器中FILE内容不完全相同,但总的来说大同小异,只要最终实现的功能相同就没什么大问题。
我们在使用fopen打开文件的时候,将这一函数返回的FILE*类型的指针返回给pf。这样,在下次读写的时候,就可以根据pf指针来寻找需要读取和写入的文件了。
最后用完pf这一指针,也可以通过pf找到需要关闭的文件进行close操作。
文件的打开和关闭
fopen
FILE* fopen(const char* filename , const char* mode);
其中filename为文件名,mode为打开方式
关于文件打开方式mode:
- “r” 以"只读"的方式打开一个文本文件(只能读),同时所读的文件必须存在。
- “r+” 与"r"的区别在于可以"写"(只能写)。
- “rb” 打开一个二进制文件(只能读),同时所读的文件必须存在。
- “rb+” 与"rb"的区别在于可以"写",同时也可以读。
- “w” 以"写"的方式创建一个文本文件,如果这个文件不存在,将创建一个此文件名的文件写入;如果存在,则将原文件内容清空并覆盖。
- “w+” 与"w"的区别在于,增加了"读"。
- “wb” 以"写"的方式创建一个二进制文件。
- “wb+” 与"wb"的区别在于,增加了"读"。
- “a” 以"尾部追加"的方式打开一个文本文件(只能写)。
- “a+” 与"a"的区别在于,增加了"读"。
- “ab” 以"尾部追加"的方式打开一个二进制文件(只能写)。
- “ab+” 与"ab"的区别在于,增加了"读"。
下面写一份代码观察一下
#include<stdio.h> int main() { //打开文件 FILE* pf = fopen("data.txt", "w"); if (pf == NULL) {//检查文件是否正常打开 perror("fopen");//如没有正常打开,打印错误原因 return 1; } //写文件 //。。。 //关闭文件 fclose(pf); pf = NULL; return 0; }
上述代码便是用只写的方式打开的
下面讲一点拓展知识,在你实际操作的过程中,也许你注意到data.txt的路径是在当前文件下的,但是,如果你想操作的文件不再当前路径或者想生成的文件不再当前路径怎么办呢?这时候,就可以使用我们的相对路径。
在文件操作中 . 表示当前目录,而 .. 表示上一级路径
比如说:./../../data.txt 表示的便是返回上两级路径。
文件的顺序读写和随机读写
文件的顺序读写
和gets,scanf和printf等函数一样,文件的读写也有其对应的函数,下面给大家列出来,然后两两讲解
1.fgetc和fputc
2.fgets和fputs
3.fscanf和fprintf
4.fread和fwrite
前三组可针对所有输入输出流
最后一组只能针对二进制文件输入输出
fgetc和fputc
int fgetc(FILE* stream);
int fputc(int charater,FILE* stream);
为了方便,我们先讲fputs,下面见代码
#include<stdio.h> int main() { FILE* pf = fopen("data.txt", "w"); if (pf == NULL) { perror("fopen"); return 1; } //写文件 for (int i = 0; i < 26; i++) { fputc('a' + i, pf); fputc('\n', pf); } fclose(pf); pf == NULL; return 0; }
可以在代码里运用fput来往文件中写字符 ,以下是写入文件中的结果
然后就是fgetc,大家应该就能猜到其作用了,这个函数可以从文件中一个个读取字符,每读取一个字符光标往后移动一位,见具体使用代码。
#include<stdio.h> int main() { FILE* pf = fopen("data.txt", "r"); if (pf == NULL) { perror("fopen"); return 1; } int ch = fgetc(pf); printf("%c\n", ch); fclose(pf); pf = NULL; return 0; }
读取上次在fputc的时候写入的数据,就可以通过data.txt文件打印出a
下面展示一份可以将一个文件中的内容转移到另一个文件中的代码,也是关于,fgetc和fputc的运用
#include<stdio.h> int main() { FILE* pfread = fopen("data1.txt", "r"); if (pfread == NULL) { perror("fopen->data1.txt"); return 1; } FILE* pfwrite = fopen("data2.txt", "w"); if (pfwrite == NULL) { perror("fopen->data2.txt"); return 1; } char ch; while ((ch = fgetc(pfread)) != EOF)fputc(ch, pfwrite); fclose(pfwrite); fclose(pfread); pfread = pfwrite = NULL; return 0; }
其中data1.txt中的文件是我随意粘上去的,最后全部写入了data2.txt中,这个代码功能还是很强大的。
fgets和fputs
char* fgets(char* str,int num,FILE* stream);读字符串(最多读num个包含'\0')
int fputs(const char* str,FILE* stream);写字符串(一行)
其中,str表示的就是字符串的读入和写出
#include<stdio.h> int main() { FILE* pfread = fopen("data1.txt", "r"); if (pfread == NULL) { perror("fopen->data1.txt"); return 1; } FILE* pfwrite = fopen("data2.txt", "w"); if (pfwrite == NULL) { perror("fopen->data2.txt"); return 1; } char arr[20]; fgets(arr, 19, pfread); fputs(arr, pfwrite); fclose(pfwrite); fclose(pfread); pfread = pfwrite = NULL; return 0; }
上述代码是将从data1.txt中读到的一行字符写入data2.txt中。
fscanf和fprintf
int fprintf(FILE* stream,const char* format……);
int fscanf(FILE* stream,const char* format……);
在我看来,这两个函数是我介绍的io函数中功能相对强大的,这两个函数的功能与printf和scanf极其相似,只是多了前面的stream文件指针,可以见一下下面的代码见见其功能
#include<stdio.h> struct Stu { char name[20]; int age; float score; }; int main() { struct Stu s = {0}; FILE* pf = fopen("data.txt", "r"); if (pf == NULL) { perror("fopen"); return 1; } FILE* pfw = fopen("data1.txt", "w"); if (pfw == NULL) { return 1; } fscanf(pf, "%s %d %f", s.name, &s.age, &s.score); fprintf(pfw, "%s %d %.1f", s.name, s.age, s.score); fclose(pf); pf == NULL; return 0; }
在运行结束后data.txt中的内容就成功写道data1.txt中去了 。
fread和fwrite
size_t fread(void* ptr,size_t size,size_t count,FILE* stream);
size_t fwrite(void* ptr,size_t size,size_t count,FILE* stream);
第一个参数代表位置,第二个代表大小,第三个代表个数,第四个是文件指针
下面来看代码使用
#include<stdio.h> struct Stu { char name[20]; int age; float score; }; int main() { struct Stu s = {0}; FILE* pf = fopen("data.txt", "rb+"); if (pf == NULL)return 1; fread(&s, sizeof(s), 1, pf); printf("%s %d %.1f", s.name, s.age, s.score); fwrite(&s, sizeof(s), 1, pf); fclose(pf); pf = NULL; return 0; }
最后就可以以二进制的形式将文件写入另一个文件了,只不过由于是文本文件打开的原因,其解码形式是文本文件的形式,所以数字打印出来的码是看不懂的。
文件的随机读写
文件的随机读写,意思不是随机的读写文件中的数据,而是可控的控制光标读写入和读取文件中的数据。相关的函数有,fseek,ftell和rewind。
fseek
int fseek(FILE* stream,long int offset,int origin);
其中stream表示的是文件指针,offset表示的是光标偏移量,origin可以控制光标的起始位置
ftell
long int ftell(FILE* stream);
stream表示文件指针
rewind
void rewind(FILE* stream);
stream表示文件指针
使用见代码
#include<stdio.h> int main() { FILE* pf = fopen("data.txt", "r"); if (pf == NULL) { return 1; } fseek(pf, -4, SEEK_CUR);//从当前光标向前移动4位 fseek(pf, -6, SEEK_END);//从末尾光标向前移动6位 fseek(pf, 0, SEEK_SET);//将光标移到初始位 int n = ftell(pf);//返回光标相对起始位置的偏移量 rewind(pf);//将文件指针返回起始位置 fclose(pf); pf = NULL; return 0; }
关于对文件读取结束的判定
错误使用feof:不能用其返回值来判断文件是否结束,feof是用来判断文件是否是因为文件正常读到文件尾而结束。
判断是否结束可以判断(EOF)fgetc,或(NULL)fgets
fread函数返回值是其读取元素个数
文件缓冲区
ANSIC采用“缓冲区系统”处理数据文件
系统自动为程序每一个正在使用的文件开辟一块“文件缓冲区”
刷新缓冲区函数
fflush(FILE* stream);
注:fclose也有刷新缓冲区的效果
因为有缓冲区的存在,C语言在操作文件的时候,需要刷新缓冲区或者在文件操作结束时关闭文件。如果不做,可能会导致读写文件相关的问题。
结语
今天分享了关于文件操作相关的知识,创作不易,如果觉得本篇博客内容还不错的话,还请点小小的赞再走,也可以收藏以下本篇博客,如果感兴趣的话还可以给我点个关注,后续还会分享更多有意思的内容---比心♥