目录
- 1. 什么是文件?
- 2. 二进制文件和文本文件
- 3. 文件的打开和关闭
- 3.1 流和标准流
- 3.1.1 流
- 3.1.2 标准流
- 3.2 文件指针
- 3.3 打开、关闭文件
- 3.3.1 fopen - 打开文件
- 3.3.2 fclose - 关闭文件
- 4. 文件的顺序读写
- 4.1 fgetc - 从文件流获取一个字符
- 4.2 fputc - 将一个字符写入文件流
- 4.3 fgets - 从文件流获取一个字符串
- 4.4 fputs - 将一个字符串写入文件流
- 4.5 fscanf - 格式化输入
- 4.6 fprintf - 格式化输出
- 4.7 fwrite - 写入到文件
- 4.8 fread - 从文件读取
- 5. 文件的随机读写
- 5.1 fseek - 移动光标
- 5.2 ftell - 返回偏移量
- 5.3 rewind - 重置光标位置
- 6. 文件读取结束的判定
- 6.1 feof - 检查文件结尾
- 7. 文件缓冲区
正文开始
1. 什么是文件?
使用文件来持久化的保存数据,在程序设计中,根据文件功能,我们可以分为两种文件:程序文件、数据文件
- 程序文件:包括源文件(后缀为.c),目标文件(windows环境后缀为.obj),可执行文件(windows环境后缀为.exe)
- 数据文件:文件的内容为程序运行时读写的数据
本文讨论的是数据文件
之前我们处理数据的输入和输出都是以终端为对象的,即从终端输入数据,运行结果显示到显示器上
而以文件为对象处理数据的输入和输出,就是将数据输出到磁盘上,并且需要时从磁盘上把数据读取到内存中使用
文件名:为了用户识别和引用文件,一个文件要有一个唯一的文件标识。
文件名包含三部分:文件路径+文件名主干+文件后缀例如:
2. 二进制文件和文本文件
从数据的组织形式的角度来看,数据文件分为文本文件或者二进制文件
- 二进制文件:数据在内存中以二进制的形式存储,若不加转换的输出到外存(磁盘)的文件中,就是二进制文件(例如目标文件)。
- 文本文件:数据在内存中以二进制的形式存储,若以ASCII字符的形式转换并存储的文件就是文本文件(就比如记事本创建的文本文档)。
一个数据在文件中的存储分为两种情况:
- 字符数据:一律以对应的ASCII码的形式存储
- 数值型数据:既可以看作字符,使用ASCII码形式存储,也可以看作数,使用二进制形式存储。例如数值10000的存储方式如下:
3. 文件的打开和关闭
3.1 流和标准流
3.1.1 流
流是一个抽象的概念,用来描述数据从源到目的地的过程。流分为输出流和输入流。一般情况下,我们要想从流里写数据,或者从流中读取数据,都是要打开流,然后操作。
3.1.2 标准流
当我们启动C语言程序时,默认打开了三个标准流:
- stdin - 标准输入流,在大多数环境中从键盘输入,scanf 函数就是从标准输入流中读取数据。
- stdout - 标准输出流,大多数的环境中输出至显示器界面,printf 函数就是将信息输出到标准流中。
- stderr - 标准错误流,大多数环境中输出到显示器界面
这是默认打开的三个标准流,我们使用 scanf、printf 等函数就可以直接进行输入输出操作
上述三个流的类型是:FILE*,称为文件指针。C语言中,就是通过文件指针来维护流的各种操作。
3.2 文件指针
每个文件在被使用时都会在内存中开辟一个相应的文件信息区,用来存放文件的相关信息(比如文件的名字,文件的状态以及文件当前的位置等),就像是文件的一张身份证。这些信息保存在一个结构体变量中,由系统声明,并命名为 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* pf;//文件指针变量
定义pf,一个指向 FILE 类型数据的指针变量,可以使pf指向某个文件的文件信息区。通过这个文件的文件信息区中的信息就能访问该文件。也就是说,通过文件指针变量能够间接找到与它关联的文件
3.3 打开、关闭文件
3.3.1 fopen - 打开文件
函数原型:
FILE *fopen( const char *filename, const char *mode );
作用:打开文件。该函数和下文所将函数使用均须引用头文件stdio.h>>>详情戳我
fopen 用法:
- mode用来确定文件访问模式
- fopen 函数按照指定模式,打开filename所指向的文件
- 若打开成功,则返回指向新文件流的指针;若打开失败,则返回空指针
mode表示文件访问模式:
mode | 含义 | 若指定文件已存在 | 若指定文件不存在 |
---|---|---|---|
“r”(只读) | 打开文本文件以读取 | 从头读 | 出错 |
“w”(只写) | 打开文本文件以写入 | 销毁內容 | 创建新文件 |
“a”(追加) | 向文本文件末尾添加数据 | 写到结尾 | 创建新文件 |
“rb”(只读) | 打开二进制文件以读取 | 从头读 | 出错 |
“wb”(只写) | 打开二进制文件以写入 | 销毁內容 | 创建新文件 |
“ab”(追加) | 向二进制文件末尾添加数据 | 写到结尾 | 创建新文件 |
“r+”(读写) | 打开文件以读/写 | 从头读 | 错误 |
“w+”(读写) | 打开文件以读/写 | 销毁内容 | 创建新文件 |
“a+”(读写) | 打开文件以在末尾读/写 | 写到结尾 | 创建新文件 |
“rb+”(读写) | 打开二进制文件以读/写 | 从头读 | 错误 |
“wb+”(读写) | 打开二进制文件以读/写 | 销毁内容 | 创建新文件 |
“ab+”(读写) | 打开二进制文件以在末尾读/写 | 写到结尾 | 创建新文件 |
3.3.2 fclose - 关闭文件
函数原型:
int fclose( FILE *stream );
作用:关闭已打开的文件
fclose 用法:
- 关闭 stream 文件流
- 关闭成功返回0;关闭失败返回EOF
例如:
#include <stdio.h>
int main()
{FILE *pf;//打开文件pf = fopen("C/test.txt","w");//判断是否打开成功if(pf == NULL){return 1;}//使用文件//...//关闭文件fclose(pf);pf = NULL;return 0;
}
4. 文件的顺序读写
当我们打开文件并进行操作的时候,常用的操作就是对文件进行读和写,而顺序读写就是按照文件中数据的顺序来以此读写,这表现为文件光标位置随读写操作变动,即每读取或写入一个字符都会将光标向后移动一个字符
注:在程序中使用文件的数据叫做”读“(输入);将程序中数据写入进文件叫”写“(输出)
下面我们来学习一下相关函数:
函数名 | 功能 | 适用于 |
---|---|---|
fgetc | 字符输入函数 | 所有输入流 |
fputc | 字符输出函数 | 所有输出流 |
fgets | 文本行输入函数 | 所有输入流 |
fputs | 文本行输出函数 | 所有输出流 |
fscanf | 格式化输入函数 | 所有输入流 |
fprintf | 格式化输出函数 | 所有输出流 |
fread | 二进制输入 | 文件 |
fwrite | 二进制输出 | 文件 |
上面所说适用于所有输入流一般指适用于标准输入流和其他输入流(如文件输入流);所有输出流一般指适用于标准输出流和其他输出流(如文件输出流)
fread 和 fwrite 是以二进制形式输入输出,并且只能对文件流进行操作;而其他函数都是以文本的形式输入输出,并且可以对所有流进行操作
4.1 fgetc - 从文件流获取一个字符
函数原型:
int fgetc( FILE *stream );
作用:从给定的输入流读取下一个字符
fgetc 用法:
- 读取stream流中的下一个字符
- 读取成功返回所读取字符的ASCII码值;读取失败或读取到文件末尾返回EOF(-1)
- 每读取一次光标都向后移动一下
例如:
#include <stdio.h>
int main()
{FILE* pf;//打开文件pf = fopen("test.txt", "r");//判断是否打开成功if (pf == NULL){perror("fopen");return 1;}//使用文件int ch = 0;while ((ch = fgetc(pf)) != EOF){printf("%c", ch);}//关闭文件fclose(pf);pf = NULL;return 0;
}
运行结果:
4.2 fputc - 将一个字符写入文件流
函数原型:
int fputc( int ch, FILE *stream );
作用:将指定字符写入指定文件流
fputc 用法:
- ch为要写入文件的字符
- stream为输出流,即指定文件
- 写入成功则返回被写入的字符的ASCII码值;写入失败则返回EOF(-1)
- 每写入一次光标向后移动一下
例如:
#include <stdio.h>
int main()
{FILE* pf;//打开文件pf = fopen("test.txt", "a");//判断是否打开成功if (pf == NULL){perror("fopen");return 1;}//使用文件int i = 0;for (i = 0; i < 10; i++){fputc('Q', pf);}//关闭文件fclose(pf);pf = NULL;return 0;
}
运行结果:
4.3 fgets - 从文件流获取一个字符串
函数原型:
char *fgets( char *str, int count, FILE *stream );
作用:从指定文件中读取指定数量的字符,并将他们存储在一个字符数组中
fgets 用法:
- str指向一个字符数组,会将读取到的字符存储进该数组
- count为要读取字符的个数
- stream为读取数据来源的文件流
- 读取成功返回str;读取失败或读取到文件末尾则返回空指针
- 该函数只能读取同一行內容,若读取到一行的末尾,则停止读取,并读取一个\n
- 每次读取光标向后移动count个字符
例如:
#include <stdio.h>
int main()
{FILE* pf;//打开文件pf = fopen("test.txt", "r");//判断是否打开成功if (pf == NULL){perror("fopen");return 1;}//使用文件char arr[20] = { 0 };fgets(arr, 20, pf);printf("%s", arr);fgets(arr, 20, pf);printf("%s", arr);//关闭文件fclose(pf);pf = NULL;return 0;
}
运行结果:
4.4 fputs - 将一个字符串写入文件流
函数原型:
int fputs( const char *str, FILE *stream );
作用:将指定字符串写入到输出流中
fputs 用法:
- str是要写入输出流的字符串
- stream是输出流
- 写入成功返回非负值;写入失败返回EOF
例如:
#include <stdio.h>
int main()
{FILE* pf;//打开文件pf = fopen("test.txt", "w");//判断是否打开成功if (pf == NULL){perror("fopen");return 1;}//使用文件fputs("Hello World!", pf);fputs("Hello wwangxu!", pf);//关闭文件fclose(pf);pf = NULL;return 0;
}
运行结果:
4.5 fscanf - 格式化输入
函数原型:
int fscanf( FILE *stream, const char *format, ... );
作用:从文件流流中格式化读取数据,并将数据赋给对应的变量
fscanf 用法:
- stream为要读取的输入文件流
- format指向要读取的格式字符,例如%d %f %c
- …为对应的接收数据的实参
- 函数返回值为被成功复制的接收参数的数量
例如:
#include <stdio.h>struct Stu
{char name[20];char sex[10];int age;
};
int main()
{struct Stu s;FILE* pf;//打开文件pf = fopen("test.txt", "r");//判断是否打开成功if (pf == NULL){perror("fopen");return 1;}//使用文件 - 从文件中读取数据并赋值给实参fscanf(pf, "%s %s %d", s.name, s.sex, &(s.age));printf("%s %s %d", s.name, s.sex, s.age);//关闭文件fclose(pf);pf = NULL;return 0;
}
运行结果:
上述代码就是将test.txt文件中的数据读取出来,分别赋给了三个结构体成员变量中
scanf、sscanf 和 fscanf 的区别:
- scanf 的输入流是标准输入流(stdin),也就是从键盘读取数据并赋给相应变量
- sscanf 的输入流是一个字符串,也就是从一个字符串中格式化读取数据并赋给相应变量
- fscanf 的输入流是文件流(stream),是从文件中读取数据并赋给相应变量
4.6 fprintf - 格式化输出
函数原型:
int fprintf( FILE *stream, const char *format, ... );
作用:把数据格式化输出到文件中
fprintf 用法:
- stream为要写入的文件输出流
- format指向要读取的格式字符,例如%d %f %c
- …为要写入数据的参数
- 返回值为传输到文件流的字符数
例如:
#include <stdio.h>struct Stu
{char name[20];char sex[10];int age;
};
int main()
{struct Stu s = {"张三", "男", 18};FILE* pf;//打开文件pf = fopen("test.txt", "w");//判断是否打开成功if (pf == NULL){perror("fopen");return 1;}//使用文件fprintf(pf, "%s %s %d", s.name, s.sex, s.age);//关闭文件fclose(pf);pf = NULL;return 0;
}
运行结果:
上述代码将一个结构体的成员变量格式化的输出到了指定的文件流中,也就是将s的成员变量中的数据存储进test.txt文件中
printf、sprintf 和 fprintf 的区别:
- printf 是将数据格式化的输出到标准输出流(stdout),也就是输出到屏幕上
- sprintf 是将数据转换为字符,并输出到指定字符串中
- fprintf 是将数据格式化的输出到文件流(stream),也就是输出到文件中
4.7 fwrite - 写入到文件
函数原型:
size_t fwrite( const void *buffer, size_t size, size_t count, FILE *stream );
作用:从指定数组中读取数据,并以二进制的形式写入到文件中
fwrite 用法:
- stream为指向输出流的指针
- count为要被写入的对象数
- size为每个对象的大小
- buffer指向一个数组,函数会将该数组中的数据以二进制形式写入指定文件流
- 函数返回值为成功写入的对象数
例如:
#include <stdio.h>int main()
{int arr[5] = { 1,2,3,4,5 };FILE* pf;//打开文件pf = fopen("test.txt", "w");//判断是否打开成功if (pf == NULL){perror("fopen");return 1;}//使用文件int sz = sizeof(arr) / sizeof(arr[0]);fwrite(arr, sizeof(int), sz, pf);//关闭文件fclose(pf);pf = NULL;return 0;
}
运行结果:
我们可以看到,存储进文件的数据成了乱码,这是因为 fwrite 函数是以二进制的形式存储数据进文件的,而该文件是文本文件,所以会产生乱码,我们可以通过 fread 函数来查看二进制数据
4.8 fread - 从文件读取
函数原型:
size_t fread( void *buffer, size_t size, size_t count, FILE *stream );
作用:从文件中读取二进制数据并存储进指定数组中
fread 用法:
- stream为读取来源的输入文件流
- count为读取的对象的个数
- size为每个对象的大小,单位是字节
- buffer是一个指向数组的指针,读取到的数据将会存储在这个数组中
- 函数返回值为成功读取的对象数
例如:
#include <stdio.h>
int main()
{int arr[5] = { 1,2,3,4,5 };int arr2[5] = { 0 };FILE* pf;//打开文件pf = fopen("test.txt", "r");//判断是否打开成功if (pf == NULL){perror("fopen");return 1;}//使用文件//写入数据//int sz = sizeof(arr) / sizeof(arr[0]);//fwrite(arr, sizeof(int), sz, pf);// 读取文件內容fread(arr2, sizeof(int), 5, pf);int i = 0;for (i = 0; i < 5; i++){printf("%d ", arr2[i]);}//关闭文件fclose(pf);pf = NULL;return 0;
}
运行结果:
5. 文件的随机读写
上面我们所讲的都是文件的顺序读写,也就是每读写一个对象,光标都会对应向后移动。接下来我们来学习一下文件的随机读写,这可以让我们对数据进行精准的定位并操作。
5.1 fseek - 移动光标
函数原型:
int fseek( FILE *stream, long offset, int origin );
作用:在指定文件流中移动文件位置指示器(光标)
fseek 用法:
- stream为要修改的文件流
- offset为光标的偏移量
- origin为光标的起始位置,它有以下三个值:
- SEEK_SET:文件的起始位置
- SEEK_CUR:文件指针当前位置
- SEEK_END:文件末尾
- 成功时返回0,失败返回非零
例如:
#include <stdio.h>int main()
{char str[30];//打开文件FILE* pf = fopen("test.txt", "wb+");//验证是否打开成功if (pf == NULL){perror("fopen");return 1;}//写入fputs("I am wwangxu", pf);//移动光标fseek(pf, 4, SEEK_SET);//输出int i = 0;while (fread(&str[i], sizeof(char), 1, pf)){printf("%c", str[i]);i++;}//关闭文件fclose(pf);pf = NULL;return 0;
}
运行结果:
5.2 ftell - 返回偏移量
函数原型:
long ftell( FILE *stream );
作用:返回stream所指文件的文件位置指示器相对于起始位置的偏移量
例如:
#include <stdio.h>int main()
{char str[30];//打开文件FILE* pf = fopen("test.txt", "wb+");//验证是否打开成功if (pf == NULL){perror("fopen");return 1;}//写入fputs("I am wwangxu", pf);//移动光标到文件末尾fseek(pf, 0, SEEK_END);//获取了文件数据的数量int x = ftell(pf);printf("%d\n", x);//关闭文件fclose(pf);pf = NULL;return 0;
}
运行结果:
5.3 rewind - 重置光标位置
函数原型:
void rewind( FILE *stream );
作用:让stream所指向文件的文件位置指示器的位置回到文件的起始位置
例如:
#include <stdio.h>int main()
{char str[30];//打开文件FILE* pf = fopen("test.txt", "w+");//验证是否打开成功if (pf == NULL){perror("fopen");return 1;}//写入fputs("I am wwangxu", pf);//移动光标fseek(pf, 5, SEEK_SET);//输出int ch1 = fgetc(pf);printf("%c\n", ch1);//重置光标位置rewind(pf);//输出int ch2 = fgetc(pf);printf("%c\n", ch2);//关闭文件fclose(pf);pf = NULL;return 0;
}
运行结果:
6. 文件读取结束的判定
文件读取结束有两种情况:
- 读取到文件末尾,正常结束(使用feof函数判断)
- 读取发生错误(使用ferror函数判断)
6.1 feof - 检查文件结尾
函数原型:
int feof( FILE *stream );
作用:检查文件是否是因为读取到文件末尾而结束的
feof 用法:
- 检查对stream指向的文件是否读取到文件末尾
- 若读取到文件流末尾则返回非零值,否则为0
例如:
#include <stdio.h>
int main()
{char str[30];//打开文件FILE* pf = fopen("test.txt", "r");//验证是否打开成功if (pf == NULL){perror("fopen");return 1;}//写入char ch = 0;for (ch = 'a'; ch <= 'z'; ch++){fputc(ch, pf);}//判断读取结束的原因if (feof(pf)){printf("遇到文件末尾,读取正常结束\n");}else if (ferror(pf)){perror("fgetc");//读取失败,异常结束,报错}//关闭文件fclose(pf);pf = NULL;return 0;
}
运行结果:
上述代码中,以“读”的方式打开文件,但是后续却又写入,这就导致读取失败,使用ferror函数来判文件是否为读取失败导致的结束,使用perror函数将错误信息打印出来。
7. 文件缓冲区
ANSIC 标准采用“缓冲文件系统”处理的数据⽂件的,所谓缓冲⽂件系统是指系统自动地在内存中为程序中每⼀个正在使用的⽂件开辟⼀块“文件缓冲区”。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才⼀起送到磁盘上。如果从磁盘向计算机读⼊数据,则从磁盘⽂件中读取数据输⼊到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的⼤⼩根据C编译系统决定的。
完