1.文件指针
在文件操作中,一个关键的概念是文件类型指针,简称文件指针
每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件的名字,文件状态及文件当前的位置等)。这些信息是保存在一个结构体变量中的。该结构体类型是有系统声明的,取名FILE
这个FILE结构体在不同的编译器中的成员内容不完全相同,但大同小异。
每当打开一个文件的时候,系统会根据文件的情况自动创建一个FILE结构的变量,并填充其中的信息,使用者不必关心细节。
一般都是通过一个FILE的指针来维护这个FILE结构的变量,这样使用起来更加方便。
下面创建一个FILE*的指针变量:
FILE* pf;
2.文件的打开和关闭
文件在读写之前应该先打开文件,在使用结束后需要关闭文件
用fopen函数打开文件
FILE * fopen ( const char * filename, const char * mode ); 1
filename是要打开文件的文件名
mode是文件的打开方式
如果打开文件成功,会返回一个FILE*的指针变量指向该文件,也相当于建立了指针和文件的关系
如果打开失败,会返回一个NULL指针,所以在打开一个文件后,需要检查是否打开成功
mode打开方式如下:
文件使用方式 | 含义 | 如果指定文件不存在 |
“r”(只读) | 为了输入数据,打开一个已经存在的文本文件 | 出错 |
“w”(只写) | 为了输出数据,打开一个文本文件 | 建立一个新的文件 |
“a”(追加) | 向文本文件尾添加数据 | 建立一个新的文件 |
“rb”(只读) | 为了输入数据,打开一个二进制文件 | 出错 |
“wb”(只写) | 为了输出数据,打开一个二进制文件 | 建立一个新的文件 |
“ab”(追加) | 向一个二进制文件尾添加数据 | 出错 |
“r+”(读写) | 为了读和写,打开一个文本文件 | 出错 |
“w+”(读写) | 为了读和写,建议一个新的文件 | 建立一个新的文件 |
“a+”(读写) | 打开一个文件,在文件尾进行读写 | 建立一个新的文件 |
“rb+”(读写) | 为了读和写打开一个二进制文件 | 出错 |
“wb+”(读写) | 为了读和写,新建一个新的二进制文件 | 建立一个新的文件 |
“ab+”(读写) | 打开一个二进制文件,在文件尾进行读和写 | 建立一个新的文件 |
下面我们打开一个文件,并且使用后将其关闭
int main() { //打开文件 FILE* pf = fopen("test.txt", "w"); if (pf == NULL) { perror("fopen"); return 1; } //关闭文件 fclose(pf); pf = NULL;//最后要把文件指针赋为空 return 0; }
3.文件的读写
首先我们需要明白什么是输出,输入
在前面的内容中,我们习惯把printf叫做输出函数,把scanf叫做输入函数
其实,所谓的输入输出是对于程序(内存)而言
把内存中的数据取出来,让数据在屏幕上显示出来,这就是输出/写,因为把数据从内存中取出来
把从键盘获得的数据存到内存中,这就叫输入/读
现在有了文件这个概念,也就是内存与文件中有一种输入输出关系
我们将内存中的数据取出,放到文件中,就叫做输出/写
将数据的来源从键盘换做文件,从文件中获得数据放到内存中,就叫做输入/读
这里要好好理解输入输出,便于学习下面的读写操作
3.1文件的顺序读写
fgetc和fputc
fputc是字符输出函数,适用于所有输入流
int fputc ( int character, FILE * stream );
1
character是要输出的字符,是int类型的原因是字符以ASCII码的形式存储
stream 是指向标识输出流的FILE指针
如果输出成功,返回所写字符的ASCII值
如果输出失败,返回EOF
下面我们向test.txt中写几个字符
int main() { //打开文件 FILE* pf = fopen("test.txt", "w"); if (pf == NULL) { perror("fopen"); return 1; } //写文件 fputc('a', pf); fputc('b', pf); fputc('c', pf); fputc('d', pf); //关闭文件 fclose(pf); pf = NULL; return 0; }
打开test.txt文件,可以看见写文件成功
这里注意,因为以"w"方式打开文件,所以在每次写入文件都是从头开始写,也就是会覆盖上次写入的内容,如果想接着上次的内容写,就需要以"a"方式打开文件
fgets是字符输入函数,适用于所有输入流
int fgetc ( FILE * stream ); 1
如果读文件成功,会返回读到字符的ASCII值
如果失败,返回EOF
在FILE中有一个定位文件读取位置的一个指针,当调用fgetc后,这个定位指针会向后挪动,指向下一个字符,所以如果文件中有许多字符,多次调用fgetc函数即可
下面我们读取test.txt文件
int main() { //打开文件 FILE* pf = fopen("test.txt", "r"); if (pf == NULL) { perror("fopen"); return 1; } //读文件 int ret = fgetc(pf); printf("%c", ret); ret = fgetc(pf); printf("%c", ret); ret = fgetc(pf); printf("%c", ret); ret = fgetc(pf); printf("%c", ret); //关闭文件 fclose(pf); pf = NULL; return 0; }
成功读取
fgets和fputs
fputs是文本行输出函数,适用于所有输入流
int fputs ( const char * str, FILE * stream );
1
str是要写入文件的字符串
如果写入文件成功,返回一个非负值
如果失败,返回EOF
每写入一个字符串不会换行,如果想换行就要写入\n
下面写入2个字符串
int main() { //打开文件 FILE* pf = fopen("test.txt", "w"); if (pf == NULL) { perror("fopen"); return 1; } //写入文件 fputs("hello!\n", pf); fputs("你好!\n",pf); //关闭文件 fclose(pf); pf = NULL; return 0; }
写入成功:
fgets是文本行输入函数,适用于所有输入流
char * fgets ( char * str, int num, FILE * stream );
1
将读到的字符串放到指针str指向的空间中
num是读取的字符数(包括\0)
传参num个,其实会读取文件中num-1个字符,最后一个位置放\0
fgets只处理一行字符串,如果读的数大于某一行的长度,也不会去读下一行的字符串
fgets每处理一次,FILE中的定位指针会向后挪动,指向下一个位置
如果函数操作成功,返回str
失败返回NULL
int main() { //打开文件 FILE* pf = fopen("test.txt", "r"); if (pf == NULL) { perror("fopen"); return 1; } //读文件 char arr[20] = { 0 }; fgets(arr, 5, pf); printf("%s\n", arr); fgets(arr, 5, pf); printf("%s\n", arr); //关闭文件 fclose(pf); pf = NULL; return 0; }
fscanf和fprintf
fprintf是格式化输出函数 ,适用于所有输入流
int fprintf ( FILE * stream, const char * format, ... );
1
这个函数我们可以与printf函数对比,只是参数多了一个FILE指针,其他都相同
我们可以按照我们自己规定的格式去写入文件
下面我们用一下fprintf函数:
struct A { char name[20]; int age; float score; }; int main() { //打开文件 FILE* pf = fopen("test.txt", "w"); if (pf == NULL) { perror("fopen"); return 1; } struct A a = { "jack",18,100.0 }; fprintf(pf, "%s %d %f", a.name, a.age, a.score); //关闭文件 fclose(pf); pf = NULL; return 0; }
test.txt文件:
可以看到,文件中的格式的确是我们规定的格式。
fscanf是格式化输入函数,适用于所有输入流
int fscanf ( FILE * stream, const char * format, ... ); 1
这个函数我们可以与scanf函数对比,只是参数多了一个FILE指针
int main() { //打开文件 FILE* pf = fopen("test.txt", "r"); if (pf == NULL) { perror("fopen"); return 1; } struct A a = {0}; fscanf(pf, "%s %d %f", a.name, &(a.age), &(a.score)); printf("%s %d %f", a.name, a.age, a.score); //关闭文件 fclose(pf); pf = NULL; return 0; }
成功读取
fread和fwrite
fwrite是二进制输出函数,只适用于文件流
size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream );
1
指针ptr是要写入文件内容的地址,因为不确定是什么数据的类型,所以是void*类型
size是一个写入的每个元素的大小,单位是字节
counrt是写入元素的个数
如果写入成功,返回写入元素总数,一般情况下,这个成功的返回值与count相等
如果返回值与count不相等,就说明在写入的过程中有错误
如果size或count为0,则返回值为0
int main() { struct A a = { "jack",18,100.0 }; //打开文件 FILE* pf = fopen("test.txt", "wb"); if (pf == NULL) { perror("fopen"); return 1; } //写入文件 fwrite(&a, sizeof(struct A), 1, pf); //关闭文件 fclose(pf); pf = NULL; return 0; }
因为是二进制文件,所以在文件中的内容我们看不懂,但是机器可以读懂
fread是二进制输入函数,只适用于文件流
size_t fread ( void * ptr, size_t size, size_t count, FILE * stream );
1
将读取到的数据ptr指向的空间中
size是读取每个元素的大小,单位是字节
counrt是读取元素的个数
如果成功,返回值为读取到元素的个数,一般情况下,这个成功的返回值与count相等
如果返回值与count不相等,就说明读取时发生读取错误或到达文件末尾
如果size或 counrt为零,则该函数返回零,并且流状态和 ptr 指向的内容保持不变
int main() { struct A a = { 0 }; //打开文件 FILE* pf = fopen("test.txt", "rb"); if (pf == NULL) { perror("fopen"); return 1; } //读文件 fread(&a, sizeof(struct A), 1, pf); printf("%s %d %f", a.name, a.age, a.score); //关闭文件 fclose(pf); pf = NULL; return 0; }
成功读文件
3.2文件的随机读写
fseek
根据文件指针的位置和偏移量来定位文件指针
int fseek ( FILE * stream, long int offset, int origin );
1
offset是从origin偏移的字节数
origin是用作offset参考的位置
origin的值:
常量 | 参考位置 |
SEEK_SET | 文件开头 |
SEEK_CUR | 文件当前位置 |
SEEK_END | 文件结尾 |
int main() { FILE* pf = fopen("test.txt", "r"); if (pf == NULL) { perror("fopen"); return 1; } int ch = fgetc(pf); printf("%c\n", ch); ch = fgetc(pf); printf("%c\n", ch); ch = fgetc(pf); printf("%c\n", ch); fseek(pf, 2, SEEK_CUR); ch = fgetc(pf); printf("%c\n", ch); }
由前面知识点可知,每使用一次fgetc函数,那个“定位”指针就会往后挪动位,此时定位指针应该指向字符d,如果再使用fgetc就会得到字符d
这时,使用fseek函数,调整指针位置
fseek(pf, 2, SEEK_CUR);
1
这句代码的意思是:根据SEEK_CUR位置,向后挪动2位,这时再调用fgetc就会取出字符f
int main() { FILE* pf = fopen("test.txt", "r"); if (pf == NULL) { perror("fopen"); return 1; } int ch = fgetc(pf); printf("%c\n", ch); ch = fgetc(pf); printf("%c\n", ch); ch = fgetc(pf); printf("%c\n", ch); fseek(pf, 2, SEEK_CUR); ch = fgetc(pf); printf("%c\n", ch); }
这个函数的参数offset也可以是负数,就表示根据orgin位置向前移动定位指针
ftell
返回文件指针相对于起始位置的偏移量
long int ftell ( FILE * stream ); 1
程序中使用了3次fgetc函数,文件指针向后移动3次,所以再用ftell返回的偏移量就是3
int main() { FILE* pf = fopen("test.txt", "r"); if (pf == NULL) { perror("fopen"); return 1; } int ch = fgetc(pf); printf("%c\n", ch); ch = fgetc(pf); printf("%c\n", ch); ch = fgetc(pf); printf("%c\n", ch); //fseek(pf, 2, SEEK_CUR); int num = ftell(pf); printf("%d\n", num); }
rewind
让文件指针的位置回到文件的起始位置
void rewind ( FILE * stream );
1
程序中使用了3次fgetc函数,文件指针向后移动3次,再使用rewind函数将指针回到起始位置,所以再fgetc就得到第一个字符a
int main() { FILE* pf = fopen("test.txt", "r"); if (pf == NULL) { perror("fopen"); return 1; } int ch = fgetc(pf); printf("%c\n", ch); ch = fgetc(pf); printf("%c\n", ch); ch = fgetc(pf); printf("%c\n", ch); //fseek(pf, 2, SEEK_CUR); //int num = ftell(pf); // ch = fgetc(pf); rewind(pf); ch = fgetc(pf); printf("%c\n", ch); }
4.文本文件和二进制文件
根据数据的类型,数据文件可以分为两种:文本文件和二进制文件
数据在内存中以二进制的形式存储,如果不加转换的存储到文件中,就是二进制文件
如果要求在文件中以ASCII码的形式存储,则需要在存储前爪转换,以ASCII字符的形式存储的文件就是文本文件。
字符一律以ASCII形式存储
数值型数据既可以使用二进制形式,也可以是ASCII码形式存储
下面以10000为例:
10000的二进制:
所以10000的二进制形式存储就是这样
如果把10000以ASCII形式存储,先把10000,看作字符1和四个字符0组成的一个字符串,1的ASCII码值为49,49的二进制为00110001,0的ASCII码值为48,48二进制为00110000
5.文件读取结束的判定
文本文件判断是否读取结束:
1.fgetc判断返回值是否为EOF
2.fgets判断返回值是否为NULL
二进制文件判断是否读取结束:
fread判断返回值是否小于实际要读的个数
C语言stdio.h还有2个函数判断文件读取结束的原因:
feof函数和ferror函数
feof,若返回值为真,就说明文件正常读写遇到文件结束标志而结束的
ferror,若返回值为真,就说明文件在读取过程中出错而结束
判断文本文件读取结束原因的例子:
int main() { int c = 0; FILE* pf = fopen("test.txt", "r"); if (pf == NULL) { perror("fopen"); return 1; } while ((c = fgetc(pf)) != EOF) { putchar(c); } if (ferror(pf)) { printf("I/O error when reading\n"); } else if (feof(pf)) { printf("End of file reached successfully\n"); } fclose(pf); pf = NULL; return 0; }
判断二进制文件读取结束原因的例子:
enum { SIZE = 1 }; int main() { FILE* pf = fopen("test.txt", "rb"); if (pf == NULL) { perror("fopen"); return 1; } int ret = 0; int code = 0; code = fread(&ret, sizeof(char),SIZE , pf); if (code == SIZE) { printf("Array read successfully\n"); } else { if (ferror(pf)) printf("Error reading\n"); else if(feof(pf)) { printf("End of file reached successfully\n"); } } }
6.文件缓冲区
ANSIC 标准采用缓冲文件系统处理的数据文件的,所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块文件缓冲区
从内存向磁盘输出数据会先送到内存中的缓冲区,装
满缓冲区后才一起送到磁盘上。如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓
冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区
以输出(写文件)为例
内存中的数据不是直接就存到文件中的,数据需要先存放到一个叫做输出缓冲区的空间。
想要让输出缓冲区中的数据再存放到文件中有2种情况:
1.缓冲区满了
2.主动刷新缓冲区
想主动刷新缓冲区,要使用fflush函数
并且fclose函数在关闭文件时,也会刷新缓冲区
所以要记住:
因为有缓冲区的存在,C语言在操作文件的时候,需要做刷新缓冲区或者在文件操作结束的时候关闭文件。
如果不做,可能导致读写文件的问题。