摘要:本文将会介绍关于文件的操作,其中需要了解的数使用文件的原因以及什么是文件和管理文件,其次需要熟练掌握的是文件的读写操作步骤为:打开文件、操作文件、关闭文件。还需对其中各种读写方法进行归纳分类,掌握不同方式的读写操作。还需要掌握的细节就是各个操作函数的返回值。最后文章还会补充关于不同输入输出函数的对比,文本文件二进制文件的区别,以及文件结束的判定,文件缓冲区等拓展内容。
在过去学习过程中,可以发现在没有文件使用时,每个程序的数据都无法持久的保存下来,在运行时得出的数据虽然可以输出到屏幕上,但是当关闭程序后,数据就会随着程序的消失而释放,回收给操作系统,这样我没数据就不复存在了。在数据集较小时还是可以适用,但是当数据体量大时,将会无法处理。因此为了让数据持久化,可以把数据存放到磁盘文件,存放到数据库中。
而从操作方面分析,在以往学习的过程中,都是通过命令行终端进行交互,或者以终端作为输出的对象,有时面对大量数据时,输出查看结果并不方便,大量的数据输入也会浪费时间精力,因此,也可以通过文件的形式将数据进行更好的处理。
因此,使用文件的原因是将数据持久化,长久的记录下来,以及高效的完成输入输出,对程序结果有效处理。
那么什么是文件呢?从程序设计上谈起,可以从功能方面将文件分为两种:数据文件、程序文件。程序文件大概就是针对于记录程序代码数据的文件,像常见的源程序文件(后缀为.c)、目标文件、可执行文件(后缀为.exe)等;而数据文件是记录数据的文件,一般情况下回用来给程序运行时进行读写,或者程序运行结果后将数据输出,记录到文件中。
在这篇博客中将会介绍关于数据文件的内容。
对于每个文件,都有对自己的唯一标识,便于用户的识别和应用,为了方便,对于用户而言,文件标识会使用文件名。对于文件名的格式,一般包括三部分:文件路径 + 文件名主干 + 文件后缀,例如: C:\data\test.txt。在用户使用文件名时,一般采取两种方式,分别为**绝对路径与相对路径。**对于绝对路径是描述文件完整文件位置的路径,而相对路径是从当前目录出发,描述文件位置的路径。两种方式各有优点,需要在具体情况下具体分析。
要知道,磁盘文件是存到磁盘中的,C语言对文件进行操作时,并不是将整个文件再次开辟到相应的程序空间中,而是通过结构体FILE进行文件信息记录,通过文件指针,指向相应的FILE,管理相应的文件。
当有文件被使用时,将会在内存中开辟一个文件信息区,其中存放了文件的相关信息,比如文件名、文件状态、位置等,这些信息足以对该文件进行管理与使用,而这个文件信息区是通过结构体FILE来记录,以下是FILE的声明:
struct _iobuf {
char* _ptr;
int _cnt;
char* _base;
int _flag;
int _file;
int _charbuf;
int _bufsiz;
char* _tmpfname;
};
typedef struct _iobuf FILE;
而为了管理与维护这个FILE结构体,会使用文件指针来指向该结构体,通过该指针,可以访问该结构体的内容,并通过结构体的文件信息,对磁盘上的文件进行访问,总的来说是文件指针与文件进行了相互的关联。
FILE* pf;//文件指针变量
文件读写与喝矿泉水是极其类似的,在喝水与加水前后,需要对水瓶打开与关闭。对于文件读写操作前后,也同样需要打开文件与关闭文件。对于打开文件,将会通过库函数针对唯一的文件,按照操作选项进行打开,建立文件信息区,并返回文件指针。只需通过接受文件指针,就可以对文件进行相应操作;对于关闭文件,只需通过库函数,针对文件指针进行删除,并置空文件指针。
在ANSIC中规定,通过库函数fopen打开文件,库函数fclose来关闭文件。
//函数定义
FILE * fopen ( const char * filename, const char * mode );
作用:打开文件
参数:
filename:要打开的文件的名称的常量字符串,其值应遵循运行环境的文件名规范,并且可以包含路径
mode:打开文件的方式:
文件的使用方式 | 含义 | 如果制定文件不存在 |
---|---|---|
r | 为了输入数据,打开一个已经存在的文本文件 | 出错 |
w | 为了输出数据,打开一个文本文件 | 建立一个新的文件 |
a | 向文本文件尾添加数据 | 建立一个新的文件 |
rb | 为了输入数据,打开一个二进制文件 | 出错 |
wb | 为了输出数据,打开一个二进制文件 | 建立一个新的文件 |
ab | 向一个二进制文件尾添加数据 | 出错 |
r+ | 为了读和写,打开一个文本文件 | 出错 |
w+ | 为了读和写,建立一个新的文件 | 建立一个新的文件 |
a+ | 打开一个文件,在文件尾进行读写 | 建立一个新的文件 |
rb+ | 为了读和写打开一个二进制文件 | 出错 |
wb+ | 为了读和写,新建一个新的二进制文件 | 建立一个新的文件 |
ab+ | 打开一个二进制文件,在文件尾进行读和写 | 建立一个新的文件 |
返回值:如果成功打开该文件,则该函数将返回一个指向 FILE 对象的指针,该对象可用于在将来的操作中标识流。否则,将返回空指针。在大多数库实现中,errno 变量还设置为特定于系统的错误代码
注意事项:由于可能出现打开文件失败的情况,因此最好添加判断来查看打开文件是否成功
使用样例:
int main(){
//打开文件
//相对路径
FILE* pf = fopen("test1.txt", "w");
//绝对路径
//FILE* pf = fopen("D:\\计算机复习\\C_C++\\C++\\c--c--review\\Blogs_Filetest2.txt", "w");
//判断是否打开成功
if (NULL == pf){
perror("fopen");
return 1;
}
//写文件
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
//函数定义
int fclose ( FILE * stream );
作用:关闭文件
参数:要关闭的文件的文件指针。
返回值:如果关闭成功,则返回0,否则返回EOF
注意:在关闭后需要对文件指针赋空。
使用样例:
int main(){
//打开文件
//相对路径
FILE* pf = fopen("test1.txt", "w");
//绝对路径
//FILE* pf = fopen("D:\\计算机复习\\C_C++\\C++\\c--c--review\\Blogs_Filetest2.txt", "w");
//判断是否打开成功
if (NULL == pf){
perror("fopen");
return 1;
}
//写文件
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
在进行文件读写说明之前,不妨先讨论所谓的读写。在以往学习最常见的读写操作,也就是输入输出操作。通过scanf函数,从标准输入流键盘中获取数据输入流,将数据输入流输入到内存中,通过printf函数,将内存中的数据写入标准输出流屏幕。而文件的读取也一样,同样分为输入流与输出流。对于输入流,通过文件输入流来输入数据到内存中,对于文件输出流,文件通过程序想输出流写入数据。
补充:流
就是流动的意思。就是程序中的数据,像流水一样,流动到其他位置,但是到底流动到那块,要看你使用的是什么流;输出流:如果是标准输出流stdout,那就是流动到控制台,就是输出的内容在控制台上显示,比如printf打印。如果是文件输出流,程序中的数据就被写入到具体的文件中了;输入流:就是数据从其他位置流动到程序中,如果是标准输入流,就是通过键盘把数据输入到程序中;
文件读写主要分为两类,分别为顺序读写与随机读写。顺序读写是按照顺序比如文件头或者文件尾等位置进行读写;随机读写的意思是可以设置偏移量与起始位置完成读写。以下是对两类读写方式的具体说明。
文件的顺序读写分为4种类型:字符顺序读写、文本行顺序读写、格式化顺序读写、二进制顺序读写。
功能 | 函数名 | 适用 |
---|---|---|
字符输入函数 | fgetc | 所有输入流 |
字符输出函数 | fputc | 所有输入流 |
文本行输入函数 | fgets | 所有输入流 |
文本行输出函数 | fputs | 所有输入流 |
格式化输入函数 | fscanf | 所有输入流 |
格式化输出函数 | fprintf | 所有输入流 |
二进制输入 | fread | 文件 |
二进制输出 | fwrite | 文件 |
//函数定义:字符输入函数
int fgetc ( FILE * stream );
作用:从输入流中输入数据
参数:指向 FILE 对象的指针,该 FILE 对象标识了要在上面执行操作的流。
返回值:成功时为获得的字符,失败时为 EOF 。若文件尾条件导致失败,则另外设置 stream 上的文件尾指示器(见 feof() )。若某些其他错误导致失败,则设置 stream
上的错误指示器(见 ferror() )。
解释:
使用样例:
int main(){
//打开文件
FILE* pf = fopen("test.txt", "r");
if (NULL == pf){
perror("fopen");
return 1;
}
//读文件:持续读写,直到达到尾行
int ch = 0;
while ((ch = fgetc(pf)) != EOF){
printf("%c ", ch);
}
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
//函数定义:字符输入函数
int fputc ( int character, FILE * stream );
作用:向输出流输出数据
参数:
返回值:如果返回成功,返回写入的字符ASCⅡ码值,如果失败返回EOF,并是设置错误指示器
注意事项:在写入后,字符指示器会向前一位
使用样例:
int main(){
//打开文件
FILE* pf = fopen("test.txt", "w");
if (NULL == pf){
perror("fopen");
return 1;
}
//写文件:写入文件26个字母
int i = 0;
for (i = 0; i < 26; i++){
fputc('a'+i, pf);
}
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
//函数定义:文本行输入函数
char * fgets ( char * str, int num, FILE * stream );
//函数定义:文本行输出函数
int fputs ( const char * str, FILE * stream );
作用:与字符顺序读写函数相似,不过传的时字符串,并且是以行为单位的
参数:与字符顺序读写函数参数相似,将字符更换为字符串;对于num参数,指的是行读取字符个数,但由于会追加结束符,因此实际数目为num-1
返回值:
注意事项:当fgets返回字符串时,在最后会追加结束符;在fgets读取一行,指示器会指向下一行的位置,但是fputs不会,但需要换行时必须要在末尾添加\n。
使用样例:
//按照顺序写文本行
int main(){
//打开文件
FILE* pf = fopen("test2.txt", "w");
if (NULL == pf){
perror("fopen");
return 1;
}
//写文件-一行一行写
fputs("hellow\n", pf);
fputs("hellow world\n", pf);
//关闭文件
fclose(pf);
pf = NULL;
pf = fopen("test2.txt", "r");
//读文件-一行一行读
char arr[20] = "#################";
fgets(arr, 20, pf);
printf("%s", arr);
fgets(arr, 20, pf);
printf("%s", arr);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
//函数定义:格式化输入函数
int fscanf ( FILE * stream, const char * format, ... );
//函数定义:格式化输出函数
int fprintf ( FILE * stream, const char * format, ... );
作用:
参数:
stream:指向 FILE 对象的指针,该 FILE 对象标识了流
format:这是 C 字符串,包含了以下各项中的一个或多个:空格字符、非空格字符 和 format 说明符。format 说明符形式为 [=%[\*][width][modifiers]type=]
参数 | 描述 |
---|---|
* | 这是一个可选的星号,表示数据是从流 stream 中读取的,但是可以被忽视,即它不存储在对应的参数中。 |
width | 这指定了在当前读取操作中读取的最大字符数。 |
modifiers | 为对应的附加参数所指向的数据指定一个不同于整型(针对 d、i 和 n)、无符号整型(针对 o、u 和 x)或浮点型(针对 e、f 和 g)的大小: h :短整型(针对 d、i 和 n),或无符号短整型(针对 o、u 和 x) l :长整型(针对 d、i 和 n),或无符号长整型(针对 o、u 和 x),或双精度型(针对 e、f 和 g) L :长双精度型(针对 e、f 和 g) |
type | 一个字符,指定了要被读取的数据类型以及数据读取方式。具体参见下一个表格。 |
类型说明符补充:
类型 | 合格的输入 | 参数的类型 |
---|---|---|
c | 单个字符:读取下一个字符。如果指定了一个不为 1 的宽度 width,函数会读取 width 个字符,并通过参数传递,把它们存储在数组中连续位置。在末尾不会追加空字符。 | char * |
d | 十进制整数:数字前面的 + 或 - 号是可选的。 | int * |
e,E,f,g,G | 浮点数:包含了一个小数点、一个可选的前置符号 + 或 -、一个可选的后置字符 e 或 E,以及一个十进制数字。两个有效的实例 -732.103 和 7.12e4 | float * |
o | 八进制整数。 | int * |
s | 字符串。这将读取连续字符,直到遇到一个空格字符(空格字符可以是空白、换行和制表符)。 | char * |
u | 无符号的十进制整数。 | unsigned int * |
x,X | 十六进制整数。 | int * |
附加参数 – 根据不同的 format 字符串,函数可能需要一系列的附加参数,每个参数包含了一个要被插入的值,替换了 format 参数中指定的每个 % 标签。参数的个数应与 % 标签的个数相同。
返回值:
使用样例:
//写与读一个结构体的数据
struct Student {
char name[20];
int age;
float score;
};
int main() {
struct Student s = { 0 };
struct Student s1 = { "zhangsan", 20, 95.5f };
struct Student s2 = { "lisi", 21, 95.5f };
//把s中的数据写到文件中
FILE* pf = fopen("test.txt", "w");
if (NULL == pf) {
perror("fopen");
return 1;
}
//写文件
fprintf(pf, "%s %d %.1f", s1.name, s1.age, s1.score);
fprintf(pf, "%s %d %.1f", s2.name, s2.age, s2.score);
fclose(pf);
pf = NULL;
pf = fopen("test.txt", "r");
if (NULL == pf) {
perror("fopen");
return 1;
}
//读文件
fscanf(pf, "%s %d %f", s.name, &(s.age), &(s.score));
printf("%s %d %f\n", s.name, s.age, s.score);
fscanf(pf, "%s %d %f", s.name, &(s.age), &(s.score));
printf("%s %d %f\n", s.name, s.age, s.score);
fclose(pf);
pf = NULL;
return 0;
}
补充:使用方式与scanf、printf非常类似,只需要查看样例即可掌握;在每次读取后都会向后移动到读取位置后的末尾
//函数定义:二进制输入函数
size_t fread ( void * ptr, size_t size, size_t count, FILE * stream );
作用:给定流 stream 读取数据到 ptr 所指向的数组中。
参数:
返回值:成功读取的元素总数会以 size_t 对象返回,size_t 对象是一个整型数据类型;如果总数与 nmemb 参数不同,则可能发生了一个错误或者到达了文件末尾。
解释:当发生输入错误时,可以通过设置正确的指示器来判断错误(错误指示器将会在后续补充)。
使用样例:
//二进制的读文件
struct S{
char name[20];
int age;
float score;
};
int main(){
struct S s = {0};
//把s中的数据写到文件中
FILE* pf = fopen("test.txt", "rb");
if (NULL == pf)
{
perror("fopen");
return 1;
}
//二进制的读文件
fread(&s, sizeof(s), 1, pf);
printf("%s %d %f\n", s.name, s.age, s.score);
fclose(pf);
pf = NULL;
return 0;
}
//函数定义:二进制输出函数
size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream );
作用:把 ptr 所指向的数组中的数据写入到给定流 stream 中。
参数:
返回值:如果成功,该函数返回一个 size_t 对象,表示元素的总数,该对象是一个整型数据类型。如果该数字与 count参数不同,则会显示一个错误
解释:当发生输入错误时,可以通过设置正确的指示器来判断错误(错误指示器将会在后续补充)。
注意事项:在输出后,当使用文本文件去阅读输出内容后是无法读取的,因为这里是二进制输出,编码方式为二进制,与文本方式不同,两者的主要区别是存入数据时,是否发生转换,在后文将会对其进行介绍。
使用样例:
struct S{
char name[20];
int age;
float score;
};
int main(){
struct S s = { "zhangsan", 20, 95.5f };
//把s中的数据写到文件中
FILE*pf = fopen("test.txt", "wb");
if (NULL == pf){
perror("fopen");
return 1;
}
//二进制的写文件
fwrite(&s, sizeof(s), 1, pf);
fclose(pf);
pf = NULL;
return 0;
}
细心的读者可以发现,对于文件顺序读写的函数对适用会有不同,有些是只能对文件,而有些是可以对于所有流的。在此,将会介绍为什么对应所有的流。在此以fscanf
与fprinf
举例:
实际上对于适用于所有输入流的函数来说,只需要定义清楚流的位置就可以实现功能。比如fscanf
来实现scanf
的功能,只需要清楚输入流的位置为stdin(键盘)即可。而fprintf
实现printf
的功能只需将输出流路径设置为stdout(屏幕)即可,示例如下:
int main() {
//通过fscanf与fprintf实现输入输出 hellow world
char arr[20] = { 0 };
for (int i = 0; i <13; i++) {
fscanf(stdin, "%c", &arr[i]);
}
fprintf(stdout, "%s", arr);
return 0;
}
补充:实际上对于每一个C程序,在运行时会自动打开三个流,分别为stdin(标准输入流——键盘)、stdout(标准输出流——屏幕)、stderr(标准错误流——屏幕)。在使用适用于所有流的函数时,可以通过更改流的方向来完成功能的实现。
在此将会补充 scanf
printf
fscanf
fprintf
sscanf
sprintf
的对比,首先将各个函数的作用进行列举,再对其进行讲解。要说明的是直接记住各个函数的作用容易弄混,因此分析才会关键,理解后就非常容易的记住。
- scanf:按照一定格式,从标准输入流(键盘)输入数据
- print:按照一定格式,先标准输出流(屏幕)输出数据
- fscanf:按照一个格式从输入流输入数据
- fprintf:按照一个格式向输出流输出数据
- sscanf:从字符串中按照一个格式读取格式化的数据
- sprintf:把格式化数据按照一个格式转换为字符串
虽然对于sscanf
与sprintf
并不与流有关,但是可以通过流来理解,记住这里只是来辅助理解,并不是真正的流。
分析如下:可以看到,可以将其中的函数分为两组 ,分别包含scanf
或者printf
,两者共同点是按照一定格式,两者的区别就是输入与输出的区别。输入就是从输入流输入到程序中,输出就是向输出流输出数据。还可以将其中的函数分为三组,分别为标准系列、文件系列(f为前缀)与字符串系列(s为前缀),三者的区别就是“流对象”分别为:标准输入输出流(键盘与屏幕)、文件输入输出流、字符串输入输出流(辅助理解)。
因此在使用时,只需组合即可,比如sprintf
,为printf
系列,即输出函数,流对象为字符串,因此其作用为:将按照一定格式向字符串输出数据。
理解大概作用后,将会对函数进行具体说明:
//函数定义
int scanf ( const char * format, ... );
int printf ( const char * format, ... );
int fscanf ( FILE * stream, const char * format, ... );
int fprintf ( FILE * stream, const char * format, ... );
int sscanf ( const char * s, const char * format, ...);
int sprintf ( char * str, const char * format, ... );
参数:只是“流对象”不同,其他参数为对应的格式字符串,以及对应的变量。
返回值:返回读取个数,如果成功,会返回读取的全部个数(如果是字符串不包括结束字符),如果失败返回一个负数。
使用样例:
struct Student {
char name[20];
int age;
double score;
};
int main() {
char arr[20] = { 0 };
int i = 0;
//按照一定格式,从标准输入流(键盘)输入数据
while (scanf("%c", &arr[i]) == 1) {
if (arr[i] == '\n')
break;
i++;
}
//按照一定格式,先标准输出流(屏幕)输出数据
printf("printf:%s", arr);
FILE* pf = fopen("test.txt", "w");
struct Student student = { "zhangsan" , 20 , 50 };
//按照一个格式向输出流输出数据
fprintf(pf, "%s %d %lf", student.name, student.age, student.score);
fclose(pf);
pf = NULL;
pf = fopen("test.txt", "r");
struct Student student2 = { 0 };
//按照一个格式从输入流输入数据
fscanf(pf, "%s %d %lf", student2.name, &student2.age, &student2.score);
printf("fscanf: %s %d %lf\n", student2.name, student2.age, student2.score);
fclose(pf);
pf = NULL;
char strStudent[20] = "zhangsan 20 100";
struct Student student3 = {0};
//从字符串中按照一个格式读取格式化的数据
sscanf(strStudent, "%s %d %ld", student3.name, &student3.age, &student3.score);
printf("sprintf: %s %d %ld\n", student3.name, student3.age, student3.score);
char strStudent2[20] = { 0 };
//把格式化数据按照一个格式转换为字符串
sprintf(strStudent2,"%s %d %ld", student3.name, student3.age, student3.score);
printf("sprintf:%s\n", strStudent2);
}
文件的随机读写就是可以通过根据文件指针的位置和偏移量来定位文件指针,从而达到文件随机读写的目的。其中主要使用了fseek
ftell
rewind
三个函数,以下对其分别介绍。
//函数定义
int fseek ( FILE * stream, long int offset, int origin );
作用:设置流 stream 的文件位置为给定的偏移 offset,参数 offset 意味着从给定的 origin 位置查找的字节数。
参数:
stream – 指向 FILE 对象的指针,该 FILE 对象标识了流。
offset – 相对 origin的偏移量,以字节为单位。
whence – 表示开始添加偏移 offset 的位置。它一般指定为下列常量之一:
常量 | 描述 |
---|---|
SEEK_SET | 文件的开头 |
SEEK_CUR | 文件指针的当前位置 |
SEEK_END | 文件的末尾 |
返回值:如果成功,则该函数返回零,否则返回非零值。
使用样例:
int main(){
FILE* fp;
fp = fopen("test.txt", "w+");
fputs("C", fp);
fseek(fp, 10, SEEK_SET);
fputs("SEEK_SET", fp);
fseek(fp, 5, SEEK_CUR);
fputs("SEEK_CUR", fp);
fseek(fp, -3, SEEK_END);
fputs("SEEK_END", fp);
fclose(fp);
return(0);
}
//函数定义
long int ftell ( FILE * stream );
作用:返回给定流 stream 的当前文件位置
参数:stream – 这是指向 FILE 对象的指针,该 FILE 对象标识了流
返回值:该函数返回位置标识符的当前值。如果发生错误,则返回 -1L,全局变量 errno 被设置为一个正值
使用样例:
/* ftell example : getting size of a file */
#include
int main()
{
FILE* pFile;
long size;
pFile = fopen("test.txt", "rb");
if (pFile == NULL) perror("Error opening file");
else
{
fseek(pFile, 0, SEEK_END);
size = ftell(pFile);
fclose(pFile);
printf("Size of myfile.txt: %ld bytes.\n", size);
}
return 0;
}
//函数定义
void rewind ( FILE * stream );
作用:让文件指针的位置回到文件的起始位置
参数:stream – 这是指向 FILE 对象的指针,该 FILE 对象标识了流。
使用样例:
/* rewind example */
int main() {
int n;
FILE* pFile;
char buffer[27];
pFile = fopen("test.txt", "w+");
for (n = 'A'; n <= 'Z'; n++)
fputc(n, pFile);
rewind(pFile);
fread(buffer, 1, 26, pFile);
fclose(pFile);
buffer[26] = '\0';
puts(buffer);
return 0;
}
文本文件与二进制文件是通过文件对于数据的不同组织形式划分的,对于二进制文件来说,数据在内存中以二进制的形式存储,不需要转换就存入到外存之中,而文本文件,数据在外存中以二进制形式存储,因此在存入外存之前需要通过转换,将二进制数据转换为ASCⅡ码,再将ASCⅡ码转入外存中。以下将会通过一个小实验进行说明:
由于数值型数据既可以用ASCII形式存储,也可以使用二进制形式存储,那通过数值在不同文件类型储存的方式来说明,比如将10000分别储存到文本文件与二进制文件中,写入代码如下:
#include
#include
int main() {
int a = 10000;
FILE* pf = fopen("test1.txt", "wb");
fwrite(&a, 4, 1, pf);//二进制的形式写到文件中
fclose(pf);
pf = NULL;
pf = fopen("test2.txt", "w");
char str[20] = { 0 };
_itoa(a, str, 10);
fputs(str, pf);//文本的形式写到文件中
fclose(pf);
pf = NULL;
return 0;
}
通过记事本的方式打开两个文件,当用文本输入的文件是可以通过记事本这种文本文件浏览得到,而二进制使用记事本就无法浏览,不过可以使用VScode进行浏览。
注意看在二进制文件中,储存的是10 27 00 00 ,实际上最低位为10,最高位为00 ,这是十六进制表示形式,当们转换为二进制发现数值为:00000000 00000000 00100111 00010000 ,存储的就是十进制的10000。
而对于文本文件,以二进制的形式打开,储存的是31 30 30 30 这里对应的ASCⅡ编码为 1 0 0 0 0 ,可见该内容为转换为ASCⅡ后再储存到文件中。
示意图如下:
在文件读取过程中,可能会出现不符合与其结果发生,但是因为返回值标准类型的原因,无法得知具体读取发生的具体异常情况。但是可以通过相应的库函数来判断具体发生情况。因此该小结将会介绍当文件读取结束后的返回值,以及读取结束后的状况判断的相应函数。
- fgetc:成功时为获得的字符,失败时为 EOF 。若文件尾条件导致失败,则另外设置 stream 上的文件尾指示器(见 feof() )。若某些其他错误导致失败,则设置
stream
上的错误指示器(见 ferror() )- fgets:成功时为 str ,失败时为空指针。若遇到文件尾条件导致了失败,则设置 stream 上的文件尾指示器(见 feof() )。若某些其他错误导致了失败,则设置 stream上的错误指示器(见 ferror() )。
- fread:成功读取的元素总数会以 size_t 对象返回,size_t 对象是一个整型数据类型。如果总数与 nmemb 参数不同,则可能发生了一个错误或者到达了文件末尾。
在使用文件读取时,可以通过条件判断语句来确定文件是否正常结束,如果发生错误时,就可以使用feof
与ferror
来分别判断是达到文件末尾或者是发生了读取错误。以下将对文本文件与二进制文件进行读取结束判定解读:
//文本文件示例
#include
#include
int main(void){
int c; // 注意:int,非char,要求处理EOF
FILE* fp = fopen("test.txt", "r");
if (!fp) {
perror("File opening failed");
return EXIT_FAILURE;
}
//fgetc 当读取失败的时候或者遇到文件结束的时候,都会返回EOF
while ((c = fgetc(fp)) != EOF) {
putchar(c);
}
//读取结束的判断
if (ferror(fp))
puts("I/O error when reading");
else if (feof(fp))
puts("End of file reached successfully");
fclose(fp);
}
//二进制文件示例
#include
enum { SIZE = 5 };
int main(void)
{
double a[SIZE] = {1.,2.,3.,4.,5.};
FILE *fp = fopen("test.bin", "wb"); // 必须用二进制模式
fwrite(a, sizeof *a, SIZE, fp); // 写 double 的数组
fclose(fp);
double b[SIZE];
fp = fopen("test.bin","rb");
size_t ret_code = fread(b, sizeof *b, SIZE, fp); // 读 double 的数组
if(ret_code == SIZE) {
puts("Array read successfully, contents: ");
for(int n = 0; n < SIZE; ++n) printf("%f ", b[n]);
putchar('\n');
} else { // error handling
//读取结束的判断
if (feof(fp))
printf("Error reading test.bin: unexpected end of file\n");
else if (ferror(fp)) {
perror("Error reading test.bin");
}
}
fclose(fp);
}
ANSIC 标准采用“缓冲文件系统”处理数据文件,所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块“文件缓冲区”。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的大小根据C编译系统决定的。当然,有时不一定需要将缓冲区填满,如果当程序关闭时,或者调用部分功能时,会对缓冲区进行刷新,直接将缓冲区数据传输出去。示意图如下:
以下通过一个小实验完成证明:
#include
#include
//VS2019 WIN10环境测试
int main()
{
FILE* pf = fopen("test.txt", "w");
fputs("abcdef", pf);//先将代码放在输出缓冲区
printf("睡眠10秒-已经写数据了,打开test.txt文件,发现文件没有内容\n");
Sleep(10000);
printf("刷新缓冲区\n");
fflush(pf);//刷新缓冲区时,才将输出缓冲区的数据写到文件(磁盘)
//注:fflush 在高版本的VS上不能使用了
printf("再睡眠10秒-此时,再次打开test.txt文件,文件有内容了\n");
Sleep(10000);
fclose(pf);
//注:fclose在关闭文件的时候,也会刷新缓冲区
pf = NULL;
return 0;
}
结论:因为有缓冲区的存在,C语言在操作文件的时候,需要做刷新缓冲区或者在文件操作结束的时候关闭文件。如果不做,可能导致读写文件的问题。
补充:
- 代码将会放到: https://gitee.com/liu-hongtao-1/c–c–review.git ,欢迎查看!
- 欢迎各位点赞、评论、收藏与关注,大家的支持是我更新的动力,我会继续不断地分享更多的知识!
更新的动力,我会继续不断地分享更多的知识!