文件操作是C语言编程中不可或缺的一部分,它使得程序能够持久化存储数据,并在需要时高效读写。本文将从基础概念到实战技巧,系统讲解C语言文件操作的核心知识点,并结合代码示例帮助读者深入理解。
一. 为什么需要文件操作?
程序运行时,数据存储在内存中,一旦程序结束,内存数据就会被释放。文件操作解决了数据的持久化问题,例如:
保存用户配置:如游戏的存档和设置。
处理大规模数据:如日志文件或数据库的读写。
跨进程通信:通过文件共享数据。
二. 文件类型
先补充文件和文件名的概念:
文件:磁盘(硬盘)上的文件是文件。
文件名:也称文件标识,例如C:\code\test.txt。由文件路径、文件名主干、文件后缀三部分组成,以便用户识别和引用。
在程序设计中,我们一般讲两种文件:程序文件和数据文件(从文件功能的角度分类)。
1. 程序文件
程序文件包含三种:
源文件(.c):开发者编写的代码文件。
目标文件(.obj):编译后的中间文件。
可执行文件(.exe):链接后直接运行的程序。
2. 数据文件
文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件。这就是数据文件,也是本章讨论的重点。
数据文件包含两种:
文本文件:如.txt、.csv,内容为ASCII字符。
二进制文件:如图片、音频,内容为二进制数据。
不同数据存储的数据文件类型不同,字符型数据一律以ASCII形式存储,数值型数据可以用ASCII形式存储,也可以使用二进制形式存储。
例如,十进制整型10000,二进制为00000000 00000000 00100111 00010000。如果以ASCII码的形式输出到磁盘(文本文件),为“1” “0” “0” “0” “0”,磁盘中占用5个字节(每个字符1个字节);如果以二进制形式输出(二进制文件),为0x10,0x27,0x00,0x00,则在磁盘中只占4个字节。
测试代码:
#include <stdio.h>int main()
{int a = 10000;FILE* pf = fopen("test.txt", "wb");//wb - write binaryfwrite(&a, 4, 1, pf);//二进制的形式写到文件中fclose(pf);pf = NULL;return 0;
}
这段代码创建了一个新文件,并将整数10000以二进制形式写入文件中。运行后调试控制台没有任何结果,但是编译器已经创建了一个二进制文件(.txt),可以通过添加现有项找到该二进制文件。
我们通过选择打开方式为二进制编译器就能打开该二进制文件。
文件内容:
三. 流和标准流
程序的数据需要输出到各种外部设备,也需要从外部设备获取数据,不同的外部设备的输入输出操作各不相同。为了方便程序员对各种设备进行方便的操作,我们抽象出了流的概念。
1. 什么是流?
在C语言中,流(stream)是一个抽象概念,表示程序与外部设备(如键盘、显示器、文件、网络等)之间数据传输的通道。
可以将流想象成一条“数据河流”,数据在这条河中单向流动,因此有输入流也有输出流:
输入流:数据从外部设备(如键盘、文件)流向程序。
输出流:数据从程序流向外部设备(如显示器、文件)。
2. 流的抽象意义
流的抽象意义有两点:
1. 统一接口:不同设备的操作方式差异巨大(例如键盘输入和文件读取),但流通过统一接口(如fgetc、fprintf)屏蔽了底层细节,程序员无需关心设备的具体实现。
2. 缓冲机制:流通常与缓冲区(Buffer)结合使用。例如,数据从内存写入磁盘时,先暂存到缓冲区,缓冲区满后一次性写入,提升IO效率(计算机系统在进行输入/输出操作时的性能表现)。
3. 标准流
但是我们从键盘输入数据,向屏幕上输出,并没有打开流。是因为C语言程序启动时,默认打开三个预定义的流,称为标准流:
1. stdin - 标准输入流,通常关联键盘输入,scanf函数就是从标准输入流中读取数据。
2. stdout - 标准输出流,通常关联显示器输出,printf函数就是将信息输出到标准输出流中。
3. stderr - 标准错误流,专用于输出错误信息,默认也关联显示器。
标准流的特点:
1. 无需手动打开和关闭:程序启动时自动创建,结束时自动释放。
2. 数据类型为FILE*(称为文件指针):C语言中,就是通过FILE*的文件指针来维护流的各种操作。
四. 文件指针
每个被使用的文件,都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息,如文件的名字,文件状态以及文件当前的位置等。
这些信息被保存在一个名为FILE的结构体变量中。该结构体是由系统声明的,如VS2013编译环境下提供的stdio.h头文件中有以下的文件类型声明:
struct _iobuf
{char *_ptr;int _cnt;char *_base;int _flag;int _file;int _charbuf;int _bufsize;char *_tmpfname;
};typedef struct _iobuf FILE;
不同的编译器的FILE类型包含的内容不完全相同,但是大同小异。每当打开⼀个文件的时候,系统会根据文件的情况自动创建⼀个FILE结构的变量,并填充其中的信息,使用者不必关心细节。
⼀般都是通过⼀个FILE的指针来维护这个FILE结构的变量,这样使用起来更加方便。下面我们可以创建⼀个FILE*的指针变量:
FILE* pf;//文件指针变量
定义pf是⼀个指向FILE类型数据的指针变量。可以使pf指向某个文件的文件信息区(是⼀个结构体变量)。通过该文件信息区中的信息就能够访问该文件。也就是说,通过文件指针变量能够间接找到与它关联的文件。
示意图:
五. 文件操作核心函数
1. 打开与关闭文件
文件在读写之前应该先打开文件,在使用结束后应该关闭文件。ANSI C规定使用fopen函数来打开文件,fclose函数来关闭文件。函数原型如下:
fopen打开文件,需指定文件路径(文件名,filename)和模式(mode)。打开文件失败时,会返回NULL。
比如:
#include <stdio.h>int main()
{FILE* pf = fopen("test.txt", "r");//用只读的方式打开文件if (pf == NULL){perror("fopen");return 1;}fclose(pf);pf = NULL;return 0;
}
这里并不存在文件名为test.txt的文件,所以pf为NULL。运行结果:
文件的打开模式有以下几种:
模式 | 含义 | 文件不存在时的行为 |
"r"(只读) | 为了读取数据,打开一个已经存在的文本文件 | 出错 |
"w"(只写) | 为了写入数据,打开一个文本文件(写入会覆盖原有内容) | 建立一个新文件 |
"a"(追加) | 向文本文件尾添加数据 | 建立一个新文件 |
"rb"(只读) | 为了读取数据,打开一个已经存在的二进制文件 | 出错 |
"wb"(只写) | 为了写入数据,打开一个二进制文件 | 建立一个新文件 |
"ab"(追加) | 向二进制文件尾添加数据 | 建立一个新文件 |
"r+"(读写) | 为了读和写,打开一个文本文件 | 出错 |
"w+"(读写) | 为了读和写,建立一个新的文本文件 | 建立一个新文件 |
"a+"(读写) | 打开一个文本文件,在文件尾进行读写 | 建立一个新文件 |
"rb+"(读写) | 为了读和写,打开一个二进制文件 | 出错 |
"wb+"(读写) | 为了读和写,建立一个新的二进制文件 | 建立一个新文件 |
"ab+"(读写) | 打开一个二进制文件,在文件尾进行读写 | 建立一个新文件 |
比如:
#include <stdio.h>int main()
{int a = 10000;FILE* pf = fopen("test.txt", "w");//用只写的方式打开文件if (pf == NULL){perror("fopen");return 1;}fclose(pf);pf = NULL;return 0;
}
运行后就会发现,程序文件相同目录下生成了一个新文件,名为test.txt。
默认在当前目录下读或写,也可以通过绝对路径和相对路径让代码按照指定路径读/写文件:
绝对路径:从根目录开始,完整描述文件或目录位置的路径,以“ / ”或“ \ ” 分隔。例如,C:\User\Username\Documents\file.txt。
相对路径:相对于当前工作目录或文件位置的路径。例如,当前路径是C:\User\Username\Documents,那么file.txt的相对路径可以是..\Picture\file.jpg,表示文件file.jpg位于当前目录的上一级目录Picture中( . 表示当前目录,.. 表示上一级目录,以此类推)。
注意:代码中要连用两个反斜杠,表示一个反斜杠,防止它被解释为一个转义序列符。
2. 顺序读写函数
当我们掌握了打开和关闭文件,就要来学习如何读写文件。
顺序读写函数有以下几种:
函数名 | 功能 | 适用于 |
fgetc | 字符输入函数(读取单个字符) | 所有输入流 |
fputc | 字符输出函数(写入单个字符) | 所有输出流 |
fgets | 文本行输入函数(读取一行文本) | 所有输入流 |
fputs | 文本行输出函数(写入一行文本) | 所有输出流 |
fscanf | 格式化输入函数 | 所有输入流 |
fprintf | 格式化输出函数 | 所有输出流 |
fread | 二进制输入 | 文件输入流 |
fwrite | 二进制输出 | 文件输出流 |
上面说的适用于所有输出/入流,一般指适用于标准输出/入流和其他输出/入流(如文件输出/入流) 。前六个函数是针对文本数据进行文件的输出和输出,最后两个是针对二进制数据进行文件的输出和出入。
2.1 fputc和fgetc
fputc函数的原型:
作用:fputc函数将字符character(传递参数是字符的ACSII码值),写入stream流(指向的对应文件信息区的指针),并前进位置指示器(即光标)。
返回值:如果写入成功,会返回该字符的ACSII码值;如果写入失败,会返回EOF。
例如:
#include <stdio.h>int main()
{FILE* pf = fopen("data.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;
}
这段代码将字符a、b、c、d写入data.txt中。运行之后就会发现,程序文件相同路径下生成了一个data.txt文本文件,打开后会有如下内容:
当写入第一个字符a时,光标就会移动到a的后面,随后写入字符b,再次移动光标,以此类推。
fgetc函数的原型:
作用:fgetc函数从流中获取字符,并前进光标。
返回值:如果获取成功,会返回该字符的ACSII码值(int类型);如果获取字符失败,会返回EOF。
例如:
#include <stdio.h>int main()
{FILE* pf = fopen("data.txt", "r");if (pf == NULL){perror("fopen");return 1;}//读文件(文件原有字符串“abcd”)int ch = fgetc(pf);printf("%c\n", ch);ch = fgetc(pf);printf("%c\n", ch);ch = fgetc(pf);printf("%c\n", ch);ch = fgetc(pf);printf("%c\n", ch);//也可以通过fgetc的返回值读取文件中所有字符://while ((int ch = fgetc(pf)) != EOF)//{// printf("%c", ch);//}//关闭文件fclose(pf);pf = NULL;return 0;
}
这段代码从data.txt中读取了前四个字符,并依次打印。运行结果:
当读取第一个字符a时,光标就会移动到a的后面,随后写读取字符b,再次移动光标,以此类推。
再来看一个例子:
#include <stdio.h>int main()
{int ch = fgetc(stdin);putchar(ch);//打印一个字符,相当于printf("%c", )//getchar -- 读取一个字符,相当于scanf("%c", )return 0;
}
fgetc函数从标准输入流stdin中获取字符,putchar函数再将字符输出。运行后会发现,控制台窗口没有输出任何数据,光标停在首位。因为此时标准输入流中没有数据,我们可以通过键盘输入字符,这个字符就会进入标准输入流,并被获取和打印。运行结果:
这说明,fgetc函数适用于所有输入流,同样的也可以证明fputc函数适用于所有输出流,代码如下:
#include <stdio.h>int main()
{int ch = fgetc(stdin);fputc(ch, stdout);return 0;
}
我们通过键盘输入一个字符,对应的就会打印这个字符。运行结果:
2.2 fputs和fgets
fputs函数的原型:
作用:fputs函数将指针str指向的字符串写入流中。
返回值:如果写入成功,会返回一个非负值(non-negative value);如果写入失败,会返回EOF。 fputs函数将字符串写入流中时,遇到“ \0 ”结束写入。
例如:
#include <stdio.h>int main()
{FILE* pf = fopen("data.txt", "w");if (pf == NULL){perror("fopen");return 1;}//写文件//写入一行字符fputs("How are you?\n", pf);fputs("abcdefg\n", pf);//关闭文件fclose(pf);pf = NULL;return 0;
}
这里就会发现,data.txt多出了很多内容:
注意:
1. 写入字符串含“\n”,写入文件时就会换行。
2. 我们会发现,原本的“abcd”已经不见了,这就是“w”只写模式的特点:写入会覆盖原有内容。
fgets函数的原型:
作用:从流中读取字符,并将其作为字符串储存到str中,直到读取(num-1)个字符或者到达换行符或文件结束符(end-of-file)为止(以先发生的为准),并移动光标至读取字符的后面 。当换行符使fgets停止读取时,换行符仍被函数认为是一个有效字符,并包含在复制到str的字符串中。
返回值:如果成功,该函数返回str;如果发生读取失败,则返回的指针是空指针。
例如:
#include <stdio.h>int main()
{FILE* pf = fopen("data.txt", "r");if (pf == NULL){perror("fopen");return 1;}//读文件,data.txt中有字符串“abcdefghijk”//读取多个字符char ch[10];fgets(ch, 10, pf);//关闭文件fclose(pf);pf = NULL;return 0;
}
调试并打开监视:
这里说明,函数参数num为10时,实际上是只读取了9个有效字符,和一个“\0”。把数组大小和num改为20,再调试并打开监视:
fgets读取完所有字符后就不会再读取。
再举一个例子:
#include <stdio.h>int main()
{FILE* pf = fopen("data.txt", "r");if (pf == NULL){perror("fopen");return 1;}//读文件//data.txt中有://hello world//hahahachar ch[20];fgets(ch, 20, pf);printf("%s", ch);//关闭文件fclose(pf);pf = NULL;return 0;
}
运行结果:
并没有读取到hahaha。但是调试并打开监视:
这里ch储存了转行符,所以只要再读取一次就能打印hahaha:
2.3 fprintf和fscanf
fprintf函数的原型:
fprintf函数将具有一定格式的数据写入流中。函数参数后面有省略号,这被称为可变参数。printf函数的参数中也存在可变参数,例如:
printf("%d",10);
printf("hello");
printf("%d %s",10,:"hello");
参数的类型和数量都不同,所以函数参数用可变参数代替。fprintf的使用和printf非常相似,只是fprintf的函数参数比printf多了一个文件指针类型的流,因此使用fprintf函数并不困难,例如:
#include <stdio.h>int main()
{int age = 18;char name[20] = "zhangsan";double grades = 95.5;FILE* pf = fopen("data.txt", "w");if (pf == NULL){perror("fopen");return 1;}//写文件fprintf(pf, "%d %s %.2lf", age, name, grades);//关闭文件fclose(pf);pf = NULL;return 0;
}
运行结果:
能够按一定格式输出,也就能按一定格式输入,fscanf函数就能从文件中获取数据。
fscanf函数的原型如下:
fscanf函数和scanf函数的参数相似(fscanf函数的参数多了“FILE* stream”),使用方法也很相似,例如:
#include <stdio.h>int main()
{int age;char name[20];double grades;FILE* pf = fopen("data.txt", "r");if (pf == NULL){perror("fopen");return 1;}//读文件//文件原有://18 zhangsan 99.50//scanf("%d %s %lf", &age, name, &grades);--再加上流就是fscanf函数:fscanf(pf, "%d %s %lf", &age, name, &grades);//关闭文件fclose(pf);pf = NULL;return 0;
}
运行结果:
2.4 fwrite和fread
fwrite函数的原型:
fwrite函数将数据以二进制形式写入流中。该函数有以下参数:
1. const void* ptr: ptr指向被写的数据。
2. size_t size:被写的数据中一个元素的长度(单位是字节)。
3. size_t count:元素的个数。
4. FILE* stream:写入数据到stream流。
例如,将一个整型数组数据以二进制形式写入data.txt文件中,代码如下:
#include <stdio.h>int main()
{FILE* pf = fopen("data.txt", "wb");//以二进制写的方式打开文件if (pf == NULL){perror("fopen");return 1;}//写文件int arr[] = { 1,2,3,4,5,6,7,8,9,10 };fwrite(arr, sizeof(int), 10, pf);//关闭文件fclose(pf);pf = NULL;return 0;
}
运行后打开data.txt,会发现写入了数据,但是并不能看出来是10个数字:
用文本文件的方式打开二进制文件,数据就会变为乱码,但实际上数组中的十个整型数据已经写入了data.txt。
我们可以用fread函数读取数据验证想法,fread函数的原型如下:
fread函数从流中读取二进制数据。该函数的四个参数和fwrite函数一样。例如:
#include <stdio.h>int main()
{FILE* pf = fopen("data.txt", "rb");//以二进制只读的方式打开文件if (pf == NULL){perror("fopen");return 1;}//写文件int arr[10] = {0};fread(arr, sizeof(int), 10, pf);int i = 0;for (i = 0; i < 10; i++){printf("%d ", arr[i]);}//关闭文件fclose(pf);pf = NULL;return 0;
}
运行结果:
说明fread函数成功从data.txt中读取这十个数字,原本文件中确实储存着这十个数字。
如果让fread函数读取文本文件,就不会读取到正确数据。例如,我们在data.txt中写入十进制整型1到10,再用相同代码读取数据,运行结果:
读取数据明显不正确。所以要注意文本文件和二进制文件的区别,以及对应的数据之间的区别。
3. sscanf和sprintf
这里补充sscanf和sprintf函数。
我们介绍过printf、fprintf、scanf和fscanf函数:
scanf -- 针对stdin的格式化的输入函数
printf -- 针对stdout的格式化的输出函数
fscanf -- 针对所有输入流 格式化的输入函数
fprintf -- 针对所有输出流的格式化的输出函数
sscanf和sprintf函数能够实现有格式的数据与字符串之间的转换:
sscanf -- 从字符串中,按照格式提取格式化的数据
sprintf -- 将带有格式的数据,按照格式转化成字符串
sprintf函数的原型:
sprintf函数写格式化的数据到str指向的字符串中,也就是将格式化的数据转换成字符串。例如:
#include <stdio.h>int main()
{int age = 18;char name[20] = "zhangsan";double grades = 95.5;char buf[120] = { 0 };//printf("%d %s %.1lf", age, name, grades)sprintf(buf, "%d %s %.1lf", age, name, grades);printf("%s\n", buf);return 0;
}
运行结果:
sprintf函数将age、name和grades这三个不同类型的变量转换成了字符串并写入buf指向的字符串中。
sscanf函数的原型:
sscanf函数和sprintf函数的作用相反,sscanf函数从字符串中读取有格式的数据。例如:
#include <stdio.h>int main()
{int age = 18;char name[20] = "zhangsan";double grades = 95.5;char buf[120] = { 0 };sprintf(buf, "%d %s %.1f", age, name, grades);int age2 = 0;char name2[20] = "";double grades2 = 0;//scanf("%d %s %lf", &age2, &name2, &grades2);sscanf(buf, "%d %s %lf", &age2, &name2, &grades2);printf("%d\n", age2);printf("%s\n", name2);printf("%.1lf\n", grades2);return 0;
运行结果:
sscanf函数从buf指向的字符串中,按%d %s %lf的格式顺序读取数据,并分别存储在变量中。
4. 文件的随机读写
文件的读写,既支持顺序读写,也支持随机读写。比如文件中有“abcdef”,假设此时光标默认在f的右边,我们可以通过一些函数让光标移动到e的左侧,以此为起始位置进行读写,这就是文件的随机读写。
4.1 fseek
fseek函数的原型:
fseek函数可以根据文件指针的位置和偏移量来重新定位文件指针(即文件内容的光标)。该函数有三个参数:
1. FILE* stream:指向文件的流。
2. long int offset:相对于起始位置origin的偏移量(单位字节),向右偏移是正数,向左偏移是负数。
3. int origin:origin有三种情况:SEEK_SET(文件的起始处),SEEK_CUR(光标的当前位置),SEEK_END(文件的末尾处)。
例如,文件中有“abcdefghi” :
int ch = fgetc(pf);
printf("%c\n",ch);
ch = fgetc(pf);
printf("%c\n",ch);
这样就会打印a和b两个字符。如果想要读取a后立即读取e,就可以用fseek函数重新定位光标(光标应该在e的左侧):
fseek(pf,4,SEEK_SET);//从文件起始处,偏移量为4的地方
fseek(pf,3,SEEK_CUR);//从光标的当前位置(即a的右侧),偏移量为3的地方
fseek(pf,-5,SEEK_END);//从文件末尾处,偏移量为-5(向左偏移,为负数)的地方
这三种写法都是定位光标在e的左侧。完整代码:
#include <stdio.h>int main()
{int age;char name[20];double grades;FILE* pf = fopen("data.txt", "r");if (pf == NULL){perror("fopen");return 1;}//读文件int ch = fgetc(pf);printf("%c\n", ch);fseek(pf, 4, SEEK_SET);//fseek(pf, 3, SEEK_CUR);//fseek(pf, -5, SEEK_END);ch = fgetc(pf);printf("%c\n", ch);//关闭文件fclose(pf);pf = NULL;return 0;
}
运行结果:
4.2 ftell
ftell函数的原型:
ftell函数会返回文件指针相对于起始位置的偏移量(返回类型为long int)。
例如:
#include <stdio.h>int main()
{int age;char name[20];double grades;FILE* pf = fopen("data.txt", "r");if (pf == NULL){perror("fopen");return 1;}//读文件//文件原有内容:abcdefghiint ch = fgetc(pf);printf("%c\n", ch);fseek(pf, 4, SEEK_SET);ch = fgetc(pf);printf("%c\n", ch);printf("%ld\n", ftell(pf));//关闭文件fclose(pf);pf = NULL;return 0;
}
当第二次fgetc函数运行后,光标指在e和f之间,与起始位置的偏移量为5。
运行结果:
4.3 rewind
rewind函数的原型:
rewind函数可以让文件指针的位置回到文件的起始位置。
例如:
#include <stdio.h>int main()
{int age;char name[20];double grades;FILE* pf = fopen("data.txt", "r");if (pf == NULL){perror("fopen");return 1;}//读文件int ch = fgetc(pf);printf("%c\n", ch);fseek(pf, 4, SEEK_SET);ch = fgetc(pf);printf("%c\n", ch);rewind(pf);//光标再次回到起始位置ch = fgetc(pf);//此时读取的就是字符aprintf("%c\n", ch);//关闭文件fclose(pf);pf = NULL;return 0;
}
第二次fgetc后,rewind函数让光标再次指向了初始位置,则第三次fgetc函数读取到的字符仍然是字符a。
运行结果:
以上就是文件的随机读写中最重要的三个函数。
六. 文件读取结束的判定
在读取文件时,我们可以利用“文件是否读取结束”这一信息判断是否继续读取文件。
例如:
#include <stdio.h>int main()
{int age;char name[20];double grades;FILE* pf = fopen("data.txt", "r");if (pf == NULL){perror("fopen");return 1;}//读文件//文件有:"abcdefghi"int ch = 0;while ((ch = fgetc(pf)) != EOF){printf("%c\n", ch);}//关闭文件fclose(pf);pf = NULL;return 0;
}
运行结果:
这里就是循环判断fgetc函数的返回值是否为EOF(文件结束标志),如果返回值不是EOF,说明文件还可以正常读取,代码就会继续读取并打印数据;如果返回值是EOF,说明文件已经读取结束,跳出循环。
判定文件读取结束,一般都是判断函数的返回值:
1.文本文件(EOF/NULL):
fgetc(函数返回数据的ASCII码值):判断返回值是否为EOF。
fgets(函数返回str):判断返回值是否为NULL。
2. 二进制文件(返回值是否小于实际要读的个数):
fread:判断返回值是否小于实际要读的个数。
文件读取结束有两种情况:
1. 正常读取:遇到文件末尾而结束。
2. 异常读取:发生读取错误而结束。
想要知道是哪种情况导致文件读取结束,就需要用到feof函数和ferror函数:
int feof ( FILE * stream ):如果文件读取时遇到文件末尾,则返回非0的整型。
int ferror ( FILE * stream ):如果文件读取时发生错误,则返回非0的整型。
例如(文本文件):
int main(void)
{FILE* pf = fopen("data.txt", "r");if (pf == NULL){perror("fopen");return 1;}//fgetc 当读取失败的时候或者遇到文件结束的时候,都会返回EOFint c; // 注意:int,非char,要求处理EOFwhile ((c = fgetc(pf)) != EOF) //I/O读取文件循环{putchar(c);}//判断是什么原因结束的if (ferror(pf))puts("I/O error when reading");else if (feof(pf))puts("End of file reached successfully");fclose(pf);pf = NULL;
}
运行结果:
根据返回结果,文件读取结束是因为到达文件末尾。
例如(二进制文件):
#include <stdio.h>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 handlingif (feof(fp))printf("Error reading test.bin: unexpected end of file\n");else if (ferror(fp)) {perror("Error reading test.bin");}}fclose(fp);
}
这里就是判断fread函数的返回值(ret_code)是否小于实际要读的个数,如果不小于,代码接着打印数据, 如果小于,代码就不会再打印数据,而是用feof函数和ferror函数检测错误情况。
运行结果:
由此可知,在文件读取过程中,不能用feof函数和ferror函数的返回值直接来判断文件是否结束,正确方法应该是判断各种文件顺序写函数的返回值。
七. 文件缓冲区
ANSI C 标准采用“缓冲文件系统” 处理数据文件。
缓冲文件系统是指系统自动地在内存中为程序中每⼀个正在使用的文件开辟一块“文件缓冲区”。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才⼀起送到磁盘上。如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。
缓冲区的大小根据C编译系统决定的。
示意图:
例如:
#include <stdio.h>
#include <windows.h>
//VS2022 WIN11环境测试
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;
}
根据代码指示在不同时间段打开text.txt文件,就会发现文件内容的变化。
这里得出结论:因为有缓冲区的存在,C语言在操作文件的时候,需要做刷新缓冲区或者在文件操作结束的时候关闭文件。如果不做,可能导致读写文件的问题。
八. 总结
文件操作是C语言中实现数据持久化的核心技能。通过本文的学习,我们了解到文件的打开、读写、随机访问及错误处理等关键操作。
掌握文件操作,让你的程序真正“记住”数据!