前言:
使用文件,我们可以将数据直接存放在电脑的硬盘上,实现数据的持久化。
实现数据持久化的方法有,可以把数据存放在磁盘文件、存放到数据库等方法。相信大家看完这篇文章,能够合理运用文件的知识,掌握这种方法,对C语言的理解更加深刻。
一、什么是文件
磁盘上的文件就是文件。在程序设计中,我们一般说的文件有两种:程序文件和数据文件(文件功能角度分类)。
一个文件要有一个唯一的标识,以便用户识别和使用。
文件名包含三个部分:文件路径+文件主干+文件后缀
例如:c:\ code\ test.txt
文件标识常被称为文件名。
1.1 程序文件
源程序文件(.c文件)
目标文件(.obj文件)
可执行程序(.exe文件)
1.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;
不同的C编译器的FILE类型包含的内容不完全相同,但大同小异。
打开一个文件的时候,系统会根据文件的情况自动创建一个FILE结构的变量,并填充其中的信息,使用者不必关心细节。
一般通过FILE的指针来维护这个FILE结构的变量,这样使用起来更方便。
如:
FILE* pf;//文件指针变量
pf是一个指向FILE类型数据的指针变量,可以使pf指向某个文件的文件信息区(是一个结构体变量)。通过该文件信息区中的信息就能够访问该文件。也就是说,通过文件指针变量就能够找到与它关联的文件。
2.2 文件的打开和关闭
读写文件的时候需要的步骤:
- 打开文件
- 被打开的文件就维护一个文件信息区
- 读写文件
- 关闭文件
ANSIC规定fopen函数打开文件,fclose函数关闭文件
2.2.1 fopen(文件打开函数)
格式:(fopen(“文件名”,“文件使用方式”))
功能:
filename是打开的那个文件,mode是文件的使用方法。在当前目录下打开文件filename,文件的使用方式是mode。
返回值:操作正常返回文件指针,失败返回NULL。
补充:
在文件打开时,该指针总是指向文件的第一个字节。读写文件时,每读写一个字节后,该位置指针的值会自动加1,指向下一个字节(读n个字符,则加n)
2.2.2 fclose(关闭文件函数)
格式(fclose(文件指针)):
功能:
(如果文件指针是pf)pf指向的文件信息区中的数据存放在文件中,然后释放该文件信息区,使文件指针变量不在指向该文件对应的FILE结构(文件信息区),从而断开与文件的联系。
返回值:
操作正常返回0,失败返回EOF(-1)。
说明:
如果不关闭文件,文件信息区的数据将会丢失。
2.2.3 文件的使用方式
实例代码:
#include <stdio.h> int main() { FILE* pf; //打开文件 pf = fopen("data.txt", "W"); if (pf == NULL) { perror("fopen"); exit(0); } //文件操作 fputs("pan long jun nb2", pf);//把字符串"pan long jun nb2",写到文件里 //关闭文件 fclose(pf);//关闭文件的时候,刷新缓冲区, return 0; }
补充:
相对路径:
“data.txt”——当前目录
“·\\Debug\\data.txt”——当前目录的Debug文件里
“··\\Debug\\data.txt”——上一个目录的Debug文件里
绝对路径:
“C:\\Users\\data.txt”——直接输入文件路径
三、文件的顺序读写
补充:
标准设备文件:
标准输入文件——stdin,代表键盘
标准输出文件——stdout,表示显示器
标准错误文件——stderr,表示显示器
这几个文件都是自动打开的。
3.1 字符读写
3.1.1 fgetc(读字符函数)
格式:
功能:
从指定文件读入一个字符,该文件必须是以读或读写的方式打开的。
返回值:
调用成功返回读入的字符,失败返回文件结束符EOF.
补充:
使用fgetc函数,文件结束时返回文件结束符EOF(-1),这个在文本文件操作时不会产生问题,没有那个字符的ASCII码是-1.,但是对二进制文件操作时,-1是一个二进制数的合法值,将影响文件数据的读取。为了解决这个问题,ANSIC提供了判断文件结束的函数feof函数。
3.1.2 fputc(写字符函数)
格式:
功能:
将一个字符,写到磁盘文件上。
返回值:
调用成功,返回输出的字符,失败返回EOF。
3.2 字符串读写
3.2.1 fgets(读字符串函数)
格式:
功能:从指定文件读入num-1个字符到字符数组,最后加一个‘\0’结束符。
返回值:
在读完n-1个字符之前遇到换行和文件结束符(EOF),直接读入失败。
没有字符可读,返回NULL。
返回值是字符的首地址。
3.2.2 fputs(写字符串函数)
格式:
功能:
向指定的文件输出一个字符串,‘\0’结束符不输出,也不输出‘\n’。其实,str是字符串地址,也可以是字符串常量,字符数组名或者指针变量。
返回值:
调用成功返回0,失败返回EOF。
3.3 格式化读写
3.3.1 fscanf(读格式化函数)
格式:
fscanf(文件指针,“格式控制字符串”,地址表列)
功能:从指定的文件中按规定的格式的去读数据,然后读入地址表列中。
返回值:
返回成功读取数据的数量,失败,用feof函数去判断或者读取错误。
3.3.2 fprintf(写格式化函数)
格式:
fprintf(文件指针,“格式控制字符串”,输出表列);
功能:
将输出表列的值按指定的格式输出到文件指针里。
返回值:
成功所写字符的数量。
3.4 二进制文件读写(数据块读写)
3.4.1 fread(二进制读数据函数)
格式:
fread(内存地址,数据项字节数,数据项个数,文件指针)
功能:
从已经打开的文件中,读取count个size大小的数据,依次送入ptr指向的内存中。
返回值:
读取数据的数量
3.4.2 fwrite(二进制写数据函数)
格式:
fwrite(内存地址,数据字节数,数据个数,文件指针)
功能:
从地址ptr中取count个size大小的数据写入文件stream中。
返回值:
写入数据的数量。
3.5 函数的比较(scanf、fscanf、sscanf的差别和printf、fprintf、sprintf的差别)
scanf:从标准输入流读取格式化的数据。
printf:向标准输出流写格式化的数据。
fscanf:适用于所有输入流的格式化输入函数。
fprintf:适用于所有输出流的格式化输出函数。
sscanf:从字符串读出格式化的数据。
sprintf:将格式化的数据转换成字符串。
四、文件的随机读写
4.1 fseek函数
格式:
fseek(文件指针,位移量,起始点)
功能:改变文件指针的位置
文件指针——文件打开时返回的文件指针
位移量——以起始点为基点,向前向后移动的字节数,可以为负值。大多数C语言版本要求位移量为long int型数。
起始点——表示从何处开始计算位移量。
返回值:
操作成功返回0,否则返回EOF
案例:
#include <stdio.h> int main() { FILE* pf; //打开文件 pf = fopen("data.txt", "wb"); //操作文件 fputs("This is an apple", pf);//data.txt文件内容为This is an apple fseek(pf, 9, SEEK_SET); fputs(" sam", pf);//data.txt文件内容为This is a sampli //关闭文件 fclose(pf);//关闭文件,刷新缓冲区 return 0; }
4.2 ftell函数
格式:
功能:
返回位置指针的当前位置。
返回值:
调用成功返回long int数值,失败返回EOF。
案例:
#include <stdio.h> int main() { //打开文件 FILE* pf; pf = fopen("data.txt", "w"); if (pf == NULL) { perror("fopen"); exit(-1); } //操作文件 fputs("panlong jun", pf);//给文件赋值 int si = ftell(pf);//查看文件指针在哪里 fseek(pf, 1, SEEK_END);//定位文件指针 int ze = ftell(pf);//查看文件指针位置 printf("%d\n%d\n", si, ze);//打印11 12 fputs(" nb2", pf);//从文件指针开始给文件赋值 ze = ftell(pf);//查看文件指针位置 printf("%d\n", ze);//打印16 //关闭文件 fclose(pf); return 0; }
4.3 rewind
格式:
功能:
让文件指针的位置回到起始位置。
返回值:没有返回值
案例:
#include <stdio.h> int main() { char arr[27]; //打开文件 FILE* pf; pf = fopen("da.txt", "w+"); //写入数据不要判断,因为里面可能是0 /* if (pf == NULL) { perror("fopen"); exit(-1); }*/ int n = 65; for (n = 65; n <= 90; n++)//向文件写入26个字母 { fputc(n, pf); } rewind(pf);//文件指针回到起始位置 fread(arr, 1, 26, pf);//依次向数组读入26个1大小的数据 //关闭文件 fclose(pf); arr[26] = '\0'; puts(arr);//打印ABCDEFGHIJKLMNOPQRSTUVWXYZ return 0; }
五、文本文件和二进制文件
数据文件被称为文本文件和二进制文件。
数据在内存中以二进制的形式存储,如果不加转换的输出到外存,就是二进制文件。
数据在内存中以二进制的形式存储,如果要求在外存上以ASCII码的形式存储,则需要在存储前转换,以ASCII字符的形式存储的文件就是文本文件。
一个数据在内存中是怎么存储得?
字符一律以ASCII形式存储,数值型数据即可以用ASCII形式存储,也可以用二进制形式存储。
如有整数10000,以ASCII码形式输出到磁盘,在磁盘中占5个字节(每个字符一个字节),以二进制形式输出,在磁盘上占4个字节(vs2013测试)。
如:
文件使用二进制编译器:
案例:
#include <stdio.h> int main() { int a = 10000; FILE* pf = fopen("dat.txt", "wb"); fwrite(&a, 4, 1, pf);//二进制的形式写到文件中 fclose(pf); pf = NULL; return 0; }
六、文件读取结束的判断
6.1 feof(判断文件结束函数)
格式:
功能:
判断文件位置指针是否已到文件尾部。
返回值:
函数返回值为非0时(vc是16)表示已到文件尾部(真表示到达文件尾)
为0时,还未到文件结束处(假表示未到文件尾)
6.2 ferror(判断文件操作是否出错)
格式:
功能:
判断文件是否出错。
返回值:
返回值为非0表示出错(真表示出错)。
返回值为0时表示没有出错。
6.3被错误使用feof
注意:
文件在读取的过程中,不能用feof函数的返回值直接来判断文件是否结束。
feof的作用:当文件读取结束的时候,判断读取结束的原因——遇到文件尾结束。
文本文件读取是否结束,判断返回值是否为EOF(fgetc),或者NULL(gets)。
二进制文件的读取是否结束,判断返回值是否小于实际要读的个数(fread)。
正确的使用:
文本文件的例子:
#include <stdio.h> int main() { //打开文件 int ch;//注意:int,非char,要处理EOF FILE* pf = fopen("tas.txt", "r"); if (pf == NULL) { perror("fopen"); exit(-1); } //操作文件 //fgetc 当读取失败的时候或者遇到文件结束的时候,都会返回EOF while (ch = fgetc(pf) != EOF) { putchar(ch);//打印一个字符 } //判断是什么原因 if (ferror(pf)) { puts("I/O error when reading");//文件发生错误 } else if (feof(pf)) { puts("End of file reached successfully");//文件指针到达尾部 } //关闭文件 fclose(pf);//关闭文件,刷新文件缓冲区 return 0; }
二进制文件的例子:
#include <stdio.h> int main() { //打开文件 double a[5] = { 1.,2.,3.,4.,5. };//定义一个双精度型数组 FILE* pf = fopen("test.bin", "wb");//必须用二进制形式 //操作文件 fwrite(a, sizeof( a[0]), 5, pf);//写double 的数组到文件里去 //关闭文件 fclose(pf); //打开文件 double b[5]; pf = fopen("test.bin", "rb");//必须用二进制形式 //操作文件 size_t ret_code = fread(b, sizeof(b[0]), 5, pf);//读double数组 if (ret_code == 5)//成功读取 { puts("array read successfully,contents:"); for (int n = 0; n < 5; n++)//打印数组b里面的值 { printf("%f ", b[n]); } putchar('\n'); } else//读取失败,需要判断是什么错误 { if (ferror(pf))//文件读取发生错误 { puts("Error reading test.bin\n"); } else if (feof(pf))//成功遇到文件尾部 { puts("Error reading test.bin:unexpected end of file\n"); } } //关闭文件 fclose(pf);//关闭文件,刷新键盘缓冲区 return 0; }
屏幕打印结果:
七、文件缓冲区
ANSIC标准采用“缓冲文件系统”处理的数据文件的,所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块“文件缓冲区”。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。 缓冲区的大小根据C编译系统决定的。
如:
案例:
#include <stdio.h> #include <windows.h> int main() { //打开文件 FILE* pf = fopen("test.txt", "w"); //操作文件 fputs("abcdef", pf);//将代码放到输出缓冲区 printf("睡眠十秒——已经写数据了,打开test.txt文件,发现文件没有内容\n"); Sleep(10000);//睡眠十秒 printf("刷新缓冲区\n"); fflush(pf);//刷新缓冲区时,才将输出缓冲区的数据写到文件里(磁盘) //注:fflush函数在高版本的vs上不能使用 printf("在睡眠十秒——此时,打开test.txt文件,文件有内容\n"); Sleep(10000);//睡眠十秒钟 //关闭文件 fclose(pf);//关闭文件,也可以刷新缓冲区 pf = NULL; return 0; }
这里可以得出一个结论:
因为有缓冲区的存在,C语言在操作文件的时候,需要做刷新缓冲区或者在文件操作结束的时候关闭文件。
如果不做,可能导致读写文件的问题。
补充:
fflush函数,可以起到刷新缓冲区的作用,刷新缓冲区时,才将缓冲区的数据写到文件里(磁盘)或者读文件(磁盘);
fflush函数在高版本的vs上不能使用;
fclose函数关闭文件,也可以起到刷新缓冲区的作用。