在之前学习的时候,我们可以发现当程序运行完,我们之前保存的数据就会消失,再次运行时还得重新输入,为了使我们保存的数据在下次运行时还能使用,我们这篇文章来学习一下怎么使用文件操作,将我们的数据保存在文件中。
1.什么是文件
磁盘(硬盘)上的文件是文件
但是在程序设计中,我们一般谈的文件有两种:程序文件,数据文件
程序文件
包括源程序文件(后缀为.c),源程序编译后生成的目标文件(Windows环境后缀为.obj),可执行程序(Windows环境后缀为.exe)。
数据文件
文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件。例如创建的.txt文件。
本篇文章讨论的是对数据文件的相关操作。
在以前各章所处理数据的输入输出都是以终端为对象的,即从终端的键盘输入数据,运行结果显示到终端的屏幕上。
其实有时候我们会把信息输出到磁盘上,当需要的时候再从磁盘上把数据读取到内存中使用,这里处理的就是磁盘上的文件。
2.文件名
一个文件要有一个唯一的文件标示,以便用户识别和引用
文件名包含3部分:文件路径 + 文件名主干 + 文件后缀
例如:c:\code\test.txt
为了方便起见,文件标识常被称为文件名。
3.文件指针
在学习文件操作时,关键的概念是”文件类型指针“,简称”文件指针“
每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件的名字,文件状态及文件当前的位置等)。这些信息是保存在一个结构体变量中的。该结构体类型是由系统声明的,取名FILE。
例如,VS2008编译环境提供的 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是一个结构体,存放文件的相关信息就行了。
不同的C编译器的FILE类型包含的内容不完全相同,但是大同小异。
每当打开一个文件的时候,系统会根据文件的情况自动创建一个FILE结构体的变量,并填充其中的信息,使用者不必关心细节。
一般都是通过一个FILE的指针来维护这个FILE结构的变量,这样使用起来更加方便。
下面我们可以创建一个FILE*的指针变量
FILE* pf;//文件指针变量
定义pf是一个指向FILE类型数据的指针变量。可以使pf指向某个文件的文件信息区(是一个结构体变量)。通过该文件信息区中的信息就能够访问该文件。也就是说,通过文件指针变量能够找到与它关联的文件。
图解:
4.文件的打开与关闭
文件在读写之前,应该先打开文件,在使用结束之后应该关闭文件
在编程程序的时候,在打开文件的同时,都会返回一个FILE*的指针变量指向该文件,也相当于建立了指针和文件的关系。
ANSIC 规定使用 fopen 函数来打开文件,fclose 来关闭文件。
FILE* fopen(const char* filename, const char* mode); 返回文件信息区的地址 文件名 打开方式 int fclose (FILE* stream); 要关闭的文件流 也就是文件信息区的地址
打开方式如下
注意:当以写(含"w")的形式打开文件时,原文件的内容都会被销毁掉。
示例代码:
#include<stdio.h> int main() { //fopen返回文件信息区的地址,通过文件信息区操作文件 //打开文件 //打开成功,放回一个指向FILE类型的指针 //打开失败,返回空指针 FILE* pf = fopen("test.txt", "r");//这里文件是相对路径,也可以用绝对路径 if (pf == NULL) { //打开失败 perror("fopen:");//打印错误信息 return 1; } //写文件 //.... //关闭文件 fclose(pf); pf = NULL;//置空 return 0; }
5.文件的顺序读写
顺序读写,说的是当我们在读或写文件的时候,文件指针会自动向后移动,因此我们可以使用循环将整个文件读/写完。
函数:
输入输出 / 读和写 都是相对内存而言的
示例:
int fgetc ( FILE * stream ); int fputc ( int character, FILE * stream );
一次读/写一个字符的空间,成功返回字符的ASCII码值,失败返回EOF
//写文件 #include<stdio.h> int main() { FILE* pf = fopen("test.txt", "w"); if (fopen == NULL) { perror("fopen"); return 1; } //写文件 //把26个字母写到文件中 for(int i=0;i<26;i++) { fputc('a'+i, pf);//写一个字符 } fclose(pf); pf = NULL; return 0; } //读文件 #include<stdio.h> int main() { FILE* pf = fopen("test.txt", "r"); if (fopen == NULL) { perror("fopen"); return 1; } //读文件 //读一个字符,返回躲到字符的ASCII码值 //读取失败,或者读到文件末尾 返回EOF -1 for(int i=0;i<26;i++) { int ch = fgetc(pf); //读取完一个会让文件内容指针向后移动,可以读取全部数据,不会只读a //与pf++不同,pf指向的是整个文件信息区 printf("%c", ch); } fclose(pf); pf = NULL; return 0; }
char * fgets ( char * str, int num, FILE * stream ); int fputs ( const char * str, FILE * stream );
一次读/写一行的空间,成功返回保存一行字符的数组地址,失败返回NULL
//fputs 写一行数据 #include<stdio.h> int main() { //打开文件 FILE*pf = fopen("test.txt", "w"); if (pf == NULL) { perror("fopen"); return 1; } //写一行数据 hello world fputs("hello world\n",pf); fputs("xilanhua\n", pf);//会追加 //关闭文件 fclose(pf); pf = NULL; return 0; } //fgets 读一行数据 #include<stdio.h> int main() { //打开文件 FILE* pf = fopen("test.txt", "r");//默认在程序所在工程目录下生成 if (pf == NULL) { perror("fopen"); return 1; } //读一行数据 //char* fgets(char* str,int num, FILE* stream); //将读到的字符串拷贝放到 str字符数组中, //然后最多读num-1 个,因为第num个放 '\0' char arr[20]; fgets(arr, 5, pf);//读一行,读到 '\n' 为止 printf("%s\n", arr);//hell //关闭文件 fclose(pf); pf = NULL; return 0; }
格式化的读写
int fprintf ( FILE * stream, const char * format, ... ); ...可变参数列表 int fscanf ( FILE * stream, const char * format, ... );
可以发现,fprintf 和 fscanf 与 printf 和 scanf 的参数列表部分只是多了 FILE* stream。
示例:
//fprintf 写文件 #include<stdio.h> struct S { int n; float f; char arr[20]; }; int main() { struct S s = { 100,3.14f,"zhangsan" }; //打开文件 FILE* pf = fopen("test.txt", "w"); if (pf == NULL) { perror("fopen"); return 1; } //写文件 fprintf(pf, "%d %f %s", s.n, s.f, s.arr); //关闭文件 fclose(pf); pf = NULL; return 0; } //读文件 struct S { int n; float f; char arr[20]; }; int main() { struct S s; //打开文件 FILE* pf = fopen("test.txt", "r"); if (pf == NULL) { perror("fopen"); return 1; } //读文件 fscanf(pf, "%d %f %s", &(s.n), &(s.f), &(s.arr)); printf("%d %f %s", s.n, s.f, s.arr); //关闭文件 fclose(pf); pf = NULL; return 0; }
与 fprintf 和 fscanf 相似的还有 sprintf 和 sscanf ,前者是数据与文件之间的操作,后者是数据与字符串之间的操作。
int sprintf ( char * str, const char * format, ... ); int sscanf ( const char * s, const char * format, ...);
- sscanf : 把一个格式化的数据 写到 字符串中
- sprintf : 把 字符串中 的 格式化数据 读出来
示例:
#include<stdio.h> struct S { int n; float f; char arr[20]; }; int main() { struct S s = { 200,3.5f,"wangwu" }; //把一个结构体转换为字符串 char arr[50] = { 0 }; sprintf(arr, "%d %f %s\n", s.n, s.f, s.arr); printf("字符串的数据:%s\n", arr);//以字符串的形式打印 //把一个字符串 转换为 对应的格式化数据 struct S tmp = { 0 }; sscanf(arr, "%d %f %s", &tmp.n, &tmp.f, &tmp.arr); printf("格式化的数据:%d %f %s\n", tmp.n, tmp.f, tmp.arr); return 0; }
上面的函数适用于所有输入/输出流,那么流是什么意思:
流可以理解为水流,数据的传输像水一样流动,可以在里面 放/拿 数据。
文件,屏幕,网络,等外部设备,流知道怎么与外界设备交互,我们只需要对流进行操作就可以。
读写文件的时候:文件流
一个C语言程序会默认打开这3个流
终端设备 屏幕 标准输出流 stdout
键盘 标准输入流 stdin
屏幕 标准错误流 stderr
stdout stdin stderr 都是FILE* 的指针
所以我们不用打开键盘和关闭键盘,而文件需要打开和关闭。
使用 fputc 从标准输入流中读取数据
//标准输入流 #include<stdio.h> int main() { //从标准输入流读 int ch = fgetc(stdin); printf("%c", ch); return 0; } //标准输出流 #include<stdio.h> int main() { fputc('a', stdout);//标准输出流,打印到屏幕上 return 0; }
二级制读写文件:
size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream ); size_t fread ( void * ptr, size_t size, size_t count, FILE * stream );
示例:
//写文件 #include<stdio.h> struct S { char name[20]; int age; float score; }; int main() { struct S s = { "张三",20,95.5f }; FILE* pf = fopen("test.dat", "wb"); if (pf == NULL) { perror("fopen"); return 1; } //写文件 fwrite(&s, sizeof(struct S), 1, pf); //字符串的二级制形式与文本的形式是相同的 //整数和浮点型数据的二级制与文本形式是不同的 //之前的函数使用字符的形式保存的,是认识的 //现在是不认识的 //关闭文件 fclose(pf); pf = NULL; return 0; } //读文件 #include<stdio.h> struct S { char name[20]; int age; float score; }; int main() { struct S s = { 0 }; FILE* pf = fopen("test.dat", "rb"); if (pf == NULL) { perror("fopen"); return 1; } fread(&s, sizeof(struct S), 1, pf);//返回值是实际成功读到的元素得个数 printf("%s %d %f", s.name, s.age, s.score); //关闭文件 fclose(pf); pf = NULL; return 0; }
6.文件的随机读写
我们可以通过 改变文件指针的指向和偏移量 来实现文件的随机读取。主要使用一下三个函数:
- 根据文件的位置和偏移量来定义文件指针
int fseek ( FILE * stream, long int offset, int origin );
- 返回文件指针相对于起始位置的偏移量
long int ftell ( FILE * stream );
- 让文件指针的位置回到文件的起始位置
void rewind ( FILE * stream );
示例:
#include<stdio.h> int main() { //文件中已保存内容为abcdefg //打开文件 FILE* pf = fopen("test.txt", "r"); if (pf == NULL) { perror("fopen"); return 1; } int ch = 0; ch = fgetc(pf);//读取完,文件指针向后移 printf("%c\n", ch);//a ch = fgetc(pf); printf("%c\n", ch);//b ch = fgetc(pf); printf("%c\n", ch);//c ch = fgetc(pf); printf("%c\n", ch);//d printf("%d\n", ftell(pf));//返回文件指针想对于文件起始位置的偏移量 //我们使用fseek函数打印b fseek(pf, -3, SEEK_CUR);//文件指针当前位置 ch = fgetc(pf); printf("%c\n", ch); //b fseek(pf, 1, SEEK_SET);//文件指针起始位置 ch = fgetc(pf); printf("%c\n", ch); //b fseek(pf, -6, SEEK_END);//文件指针末尾位置 ch = fgetc(pf); printf("%c\n", ch); //b rewind(pf);//让文件指针返回到起始位置 printf("%d\n", ftell(pf));//0 //关闭文件 fclose(pf); pf = NULL; return 0; }
7.文件类型
根据数据的组织形式,数据文件被称为文本文件或者二进制文件。
数据在内存中以二进制的形式存储,如果不加转换的输出到外存,就是二进制文件。
如果要求在外存上以ASCII码的形式存储,则需要在存储前转换。以ASCII字符的形式存储的文件就是文本文件。一个数据在内存中是怎么存储的呢?
字符一律以ASCII形式存储,数值型数据既可以用ASCII形式存储,也可以使用二进制形式存储。
如有整数10000,如果以ASCII码的形式输出到磁盘,则磁盘中占用5个字节(每个字符一个字节),而二进制形式输出,则在磁盘上只占(int 类型)4个字节(VS2022测试)。
存储方式
1.二级制形式存储
代码示例:
#include<stdio.h> int main() { //打开文件 FILE* pf = fopen("test.txt", "wb"); if (pf == NULL) { perror("fopen");//打印错误信息 return 1; } //写文件 int a = 10000; fwrite(&a, sizeof(int), 1, pf); //关闭文件 fclose(pf); pf = NULL; return 0; }
我们打开文件发现是下面的内容:
不是二级制的形式 ,这是因为我们是以文本的方式打开文件的,我们可以通过添加现有项test.txt,来改变他的打开方式:
我们就可以看到
这里是小端存储,所以可以看到这样的效果。
2.用 fprintf 存储数据 (ASCII码值形式)
#include<stdio.h> int main() { //打开文件 FILE* pf = fopen("test.txt", "w"); if (pf == NULL) { perror("fopen");//打印错误信息 return 1; } //写文件 int a = 10000; fprintf(pf,"%d",a); //关闭文件 fclose(pf); pf = NULL; return 0; }
结果
这里存放的就是‘1’ ‘0’ ‘0’ ‘0’ ‘0’ 字符对应的ASCII码值(16进制)。
所以我们以文本形式打开的时候可以看到10000。
8.文件结束的判断
被错误使用的 feof
牢记:在文件读取过程中,不能用feof函数的返回值直接用来判断文件的是否结束。而是应用于当文件读取结束的时候,判断是读取失败结束,还是遇到文件尾结束。
1. 文本文件读取是否结束,判断返回值是否为EOF (fgetc),或者NULL(fgets)
例如:
getc判断是否为EOF.
fgets判断返回值是否为NULL.
2. 二进制文件的读取结束判断,判断返回值是否小于实际要读的个数。
例如:
fread 判断返回值是否小于实际要读的个数。
这些判断的条件都是与这些函数的返回值相关的。
读取函数另外一个功能:
当读取函数读取失败,返回, 同时设置一个错误状态,使用ferror来检测这个状态,
遇到文件末尾 ,返回,同时设置一个状态,使用feof来检测这个状态
feof 与 ferror
feof 如果是遇到 end of file 文件结束标志而结束的读取操作,返回非0值
ferror 如果是遇到 读出失败(错误)而结束的读取操作,返回非0值
示例:
#include<stdio.h> int main() { //文件内部abcdef //打开文件 FILE* pf = fopen("test.txt", "r"); if (pf == NULL) { perror("fopen"); return 1; } //读文件 int ch = 0; while ((ch = fgetc(pf)) != EOF) { printf("%c ",ch); } if (feof(pf))//检查是否有这样的状态设置,有返回非0 { printf("遇到文件结束标志而结束\n"); } else if(ferror(pf)) { printf("遇到文件错误为结束"); } //关闭文件 fclose(pf); pf = NULL; return 0; }
9.文件缓冲区
ANSIC标准采用"缓冲文件系统"处理的数据文件的,所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块"文件缓冲区"。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的大小根据C编译系统决定的。这让做是为了让操作系统有更多的时间去处理其他内容。
这样还是比较抽象,因为我们看不到缓冲区的存在。
我们可以通过下面一段代码来感受缓冲区的存在:
#include<stdio.h> #include<windows.h> int main() { FILE* pf = fopen("test.txt", "w"); fputs("abcdef", pf);//先将数据放在缓冲区 printf("睡眠10秒-已经写了数据,打开test.txt文件,发现文件没有内容\n"); Sleep(10000); printf("刷新缓冲区\n"); fflush(pf);//刷新缓冲区,才将输入缓冲区的数据写到文件(磁盘) printf("在睡眠10秒—此时,再打开test.txt文件,文件有内容了"); Sleep(10000); fclose(pf);//fcolse 在关闭文件的时候,也会刷新缓冲区 pf = NULL; return 0; }
fflush 会主动刷新缓冲区,让缓冲区的数据写到磁盘上,刷新后可以发现数据在文件中,之前10秒是文件是没有的。
之前我们运行完程序会发现数据已经到文件中,是因为 fclose 也会刷新缓冲区。
本篇结束