
(文件操作)

大家好,很高兴又和大家见面啦!!!
在上一篇内容中,我们共同探索了C语言文件操作的核心基础:
stdin(键盘输入)、stdout(屏幕输出)和 stderr(错误输出)
FILE* 指针如何作为程序与文件信息区的桥梁
fopen 的各种模式(r/w/a等)差异和 fclose 的资源释放重要性
这些基础知识为我们打开了文件操作的大门。现在,我们将迈出关键一步——学习如何对文件内容进行实际读写操作。本篇将系统讲解C语言提供的八大顺序读写函数,它们构成了文件内容处理的核心工具集:
fgetc/fputc 实现单个字符的精准读写
fgets/fputs 完成整行文本的高效处理
fscanf/fprintf 实现结构化数据的输入输出
fread/fwrite 应对图像、音频等非文本文件
这些函数如同文件操作的"瑞士军刀",每种工具都有其独特的使用场景和技巧。接下来,让我们直接进入正题,开始探索这些函数的奥秘与应用技巧。
在头文件 stdio.h 中,存在着一些专门对文件进行顺序读写的函数:
fgetc —— 字符输入函数,用于从所有流中获取字符fputc —— 字符输出函数,用于向所有流中写入字符fgets —— 文本输入函数,用于从所有流中获取文本(字符串)fputs —— 文本输出函数,用于向所有流中写入文本(字符串)fscanf —— 格式化输入函数,用于从所有流中获取格式化数据fprintf —— 格式化输出函数,用于向所有流中写入格式化数据fread —— 二进制输入函数,用于对二进制文件进行二进制输入fwrite —— 二进制输出函数,用于对二进制文件进行二进制输出接下来我们还是通过 cplusplus.com 网站来逐一学习这些函数;
fgetc
该函数的使用方式如下所示:
stream在该函数的用法中,我们看到了几个新的概念:
这里我们简单的介绍一下,这几个概念:
所谓的位置指示器,我们就可以理解为文件中的光标。
下面我们通过 fopen 与 fclose 这两个函数在桌面创建一个名为:data.txt 的文本文件:
void test1() {
FILE* pf = fopen("C:\\Users\\LIQIAN\\Desktop\\data.txt", "w");
if (pf == NULL) {
perror("fopen");
return;
}
fclose(pf);
pf = NULL;
}现在我们就成功的在桌面生成了一个文本文件:

可以看到,此时的文件内部是没有任何内容的,并且在文件的首行会有一个闪烁的光标,这个光标就是文件内部的位置指示器。
文件末尾指示器(feof: end-of-File indicator):指的是文件位置指示器位于文件末尾。
文件末尾指示器标志着当前的文件内容已经全部结束,后续会有详细的介绍,这里就不再展开,只做简单了解即可。
文件错误指示器(ferror: error indicator):指的是对文件的操作出现了错误。
文件错误指示器标志着当前对文件的操作出现了错误,后续会有详细的介绍,这里就不再展开,只做简单了解即可。
接下来我们可以通过在 data.txt 文件中输入一些文本内容,再通过 fgetc 来读取该文件中的内容:
void test2() {
FILE* pf = fopen("C:\\Users\\LIQIAN\\Desktop\\data.txt", "r");
if (pf == NULL) {
perror("fopen");
return;
}
while (1) {
int ch = fgetc(pf);
if (ch == EOF) {
printf("ch = %d\n", ch);
break;
}
printf("ch = %c\n", ch);
}
fclose(pf);
pf = NULL;
}现在我们需要对文件 data.txt 进行读操作,因此我们在通过 fopen 函数打开文件时,需要通过 r/r+/w+/a+ 等操作模式打开文件。这里我选择的事通过 r 模式打开文件:

在函数中我们是通过循环来依次读取文件中的内容。不难发现,每当成功读取一个字符后,下一次 fgetc 函数会获取文件中的下一个字符,直到所有的字符都被读取,最后,函数会返回 EOF 。在这次的测试结果中,我还获取了一个关键信息:
EOF 的整型值为 -1 。该函数的用法比较简单,这里我就不再继续赘述。
fputc
该函数的使用方法如下:
characterstreamcharacter EOF 并设置错误指示器在使用该函数时,我们需要区别两种写入模式:w/a
w:将写入的内容直接覆盖文件中的内容a:在文件中的内容末尾追加新内容下面我们就来分别看一下这两种写入模式。为了更好的区分两种写入模式的区别,下面我们需要再创建一个文件:data_.txt,并将 data.txt 中的文本内容写入。

这里我们是通过鼠标来实现 data_.txt 的创建以及文本的写入,之后我们分别通过两种打开模式分别向两个文件中写入字符:L。测试代码如下所示:
void test3() {
FILE* pf1 = fopen("C:\\Users\\LIQIAN\\Desktop\\data.txt", "w");
if (pf1 == NULL) {
perror("pf1");
return;
}
FILE* pf2 = fopen("C:\\Users\\LIQIAN\\Desktop\\data_.txt", "a");
if (pf2 == NULL) {
perror("pf2");
return;
}
int ch1 = fputc('L', pf1);
if (ch1 == EOF) {
printf("ch1 = %d\n", ch1);
}
else {
printf("ch1 = %c\n", ch1);
}
int ch2 = fputc('L', pf2);
if (ch2 == EOF) {
printf("ch2 = %d\n", ch2);
}
else {
printf("ch2 = %c\n", ch2);
}
fclose(pf1);
pf1 = NULL;
fclose(pf2);
pf2 = NULL;
}下面我们就来运行函数进行测试:

从测试结果中可以看到,这两种写入模式的区别:
w 进行写入时,写入的内容会直接覆盖文件中的原本内容a 进行写入时,写入的内容会直接追加到文件中的内容末尾现在我们已经区分了两种写入模式,接下来我们就来尝试着将 data_.txt 中的文本内容通过 fputc 写入到 data.txt :
void test4() {
FILE* pf1 = fopen("C:\\Users\\LIQIAN\\Desktop\\data.txt", "a");
if (pf1 == NULL) {
perror("pf1");
return;
}
FILE* pf2 = fopen("C:\\Users\\LIQIAN\\Desktop\\data_.txt", "r");
if (pf2 == NULL) {
perror("pf2");
return;
}
int ch = fgetc(pf2);
while (ch != EOF) {
ch = fputc(ch, pf1);
if (ch == EOF) {
perror("fputc");
break;
}
ch = fgetc(pf2);
}
fclose(pf1);
pf1 = NULL;
fclose(pf2);
pf2 = NULL;
}这次测试中,我们只需要将 data_.txt 中的文本内容写入到 data.txt 中,因此我们有两种方式实现:
a 模式打开文件 data.txt ,将 data_.txt 中的内容追加到文件中w 模式打开 data.txt ,完成首字符的覆盖,再通过 a 模式将剩余内容追加到文件中这里我们采取的是第一种方式,下面我们就来测试一下:

可以看到通过循环,以及 fgetc 与 fputc 的相互配合,我们成功的将 data_.txt 中的文本内容复制到了 data.txt 中。
fgets
该函数的用法为:
str :指向存储被读取的字符的字符数组指针num :被拷贝到 str 的字符的最大个数stream :指向一个识别输入流的 FILE 对象指针stream 中识别 num - 1 个字符并存储到 str 中,并自动在末尾添加一个终止符(\0): strstr 中的内容不改变str 中的内容可能已经发生改变下面我们就尝试着通过 fgets 函数将 data.txt 中的文本内容写入一个字符数组 str 中:
void test5() {
FILE* pf1 = fopen("C:\\Users\\LIQIAN\\Desktop\\data.txt", "r");
if (pf1 == NULL) {
perror("pf1");
return;
}
char str[20] = "xxxxxxxxxxxxxxxxxx";
printf("str = %s\n", str);
char* pstr = fgets(str, 8, pf1);
printf("str = %s\n", str);
printf("pstr = %s\n", pstr);
fclose(pf1);
pf1 = NULL;
}因为咱们只需要对文件中的内容进行读操作,所以我们在打开文件时使用 r 模式即可:

我们给函数中传入的 num = 8 ,但是实际函数运行的过程中,只读取了文件中的7个字符,之后在末尾自动添加了一个 \0 ,总共是8个字符。因此函数在实际使用的过程中,函数只会在文件中获取 num - 1 个字符。
从函数的返回值可以看到,此时因为函数读取成功,因此不仅改变了 str 中的内容,并且还将 str 作为返回值直接返回。
fputs
该函数的使用方法如下:
str :存储着写入流的内容的字符数组stream :指向流的文件指针str 中的字符内容写入 stream 指向的文件中 EOF ,并设置一个错误指示器该函数在使用时需要注意,函数不会将
str末尾的\0写入到文件中
下面我们就尝试着通过函数将文本内容 Hello world!!! 写入到 data.txt 中,这里有两种写入方式:
a 模式完成写入w 模式完成写入这里我们选择覆盖写入的方式实现:
void test6() {
FILE* pf1 = fopen("C:\\Users\\LIQIAN\\Desktop\\data.txt", "w");
if (pf1 == NULL) {
perror("pf1");
return;
}
char* str = "Hello\tWorld!!!\na\0b\nc";
printf("str = %s\n", str);
int ret = fputs(str, pf1);
printf("ret = %d\n", ret);
fclose(pf1);
pf1 = NULL;
}这次测试中,我们不仅测试了写入文本内容,还测试了识别 \t、\n、\0 三个空白字符:

可以看到,函数在执行的过程中,会正常识别 \t 与 \n 并将其写入到文本文件中,但是在遇到 \0 时,并不会将其进行写入,而是直接停止执行。
从程序的运行终端中我们不难发现,fputs 函数的执行过程与 printf 函数对字符串的识别过程是一致的,他们之间的区别为:
fputs 函数是将识别的内容写入到文本文件中printf 函数是将识别的内容以文本的形式输出到终端中fwrite
函数的用法如下所示:
ptr :指向大小至少为 size * count 的内存块指针,该指针被转换成 void*size:被写入的每个元素的大小,单位为字节count :被写入的元素个数,每个元素的大小为 size 字节stream :指向识别输入流的 FILE 对象指针ptr 中获取 count 个大小为 size 的元素写入 stream 中 count 不相等时,写入错误阻止函数继续执行,这时会设置错误指示器size 或者 count 二者中的其中之一为 0 时,函数返回 0 ,并且错误指示器保持不变由于该函数是用于对二进制文件进程操作的函数,因此以下几点我们需要注意:
.binfopen 打开的文件的模式需要加一个后缀 b,如 rb/wb/ab/……接下来我们就通过 fwrite 创建一个文件 data.bin 并在文件中写入 {0, 1, 2, 3, 4, 5} :
void test7() {
FILE* pf1 = fopen("C:\\Users\\LIQIAN\\Desktop\\data.bin", "wb");
if (pf1 == NULL) {
perror("pf1");
return;
}
int arr[6] = { 0, 1, 2, 3, 4, 5 };
int ret = fwrite(arr, sizeof(int), 6, pf1);
printf("ret = %d\n", ret);
for (int i = 0; i < 6; i++) {
printf("arr[%d] = %d\n", i, arr[i]);
}
fclose(pf1);
pf1 = NULL;
}这里我们通过 ret 的值来获取成功写入的元素个数:

由于写字板无法直接读取 .bin 文件,因此我们在读取文件中的内容时,需要通过二进制文件编辑器打开,这里我使用的是 Notepad++ 进行的文件读取;
上图的测试结果中,我们也不难发现数据的存储是按照 高地址存储在高位,低地址存储在低位 的存储方式进行存储,即 小端存储。
从 ret 的值可以看到,我们成功的写入了6个元素,从文件中可以看到,我们成功的通过 fwrite 正确的完成了数据的写入;
fread
该函数的用法如下:
ptr :指向大小至少为 size * count 的内存块指针,该指针被转换成 void*size:被读取的每个元素的大小,单位为字节count :被读取的元素个数,每个元素的大小为 size 字节stream :指向识别输入流的 FILE 对象指针stream 中识别 count 个大小为 size 字节的元素,并将其存储在 ptr 指向的内存块中 count 不相等时,会被设置合适的指针指示器: ferror 检测feof 检测size 或者 count 二者中的其中之一为 0 时,函数返回 0 ,并且不会改变 ptr 中的内容以及 stream 的状态下面我们就尝试着通过 fread 函数从 stream 中获取一组整数 {0, 1, 2, 3, 4, 5} ,并将其存储到 ptr 指向的内存块中:
void test8() {
FILE* pf1 = fopen("C:\\Users\\LIQIAN\\Desktop\\data.bin", "rb");
if (pf1 == NULL) {
perror("pf1");
return;
}
int arr[6] = { 0 };
int ret = fread(arr, sizeof(int), 6, pf1);
printf("ret = %d\n", ret);
for (int i = 0; i < 6; i++) {
printf("arr[%d] = %d\n", i, arr[i]);
}
fclose(pf1);
pf1 = NULL;
}这一次由于我们是需要从二进制文件中读取数据,因此我们打开文件的模式采用的是:rb :

可以看到,此时我们成功的完成了从文件中读取数据,并存储在数组中。
fscanf该函数的介绍我们就不像前面的函数一样,下面我们通过函数的类比来说明该函数的使用方法:

上图展示的是 scanf 的函数定义。这个函数是我们十分熟悉的标准输入函数,我们在使用该函数进行输入时,通常采用下面的格式:
int a = 0;
char arr[10] = "";
int input = scanf("%d%s", &a, arr);简单的说明就是:

上图展示的是 fscanf 的函数介绍,从函数定义可以看到,相比于 scanf ,该函数多了一个参数:stream 。
这个参数的含义也很简单,就是由 scanf 函数从标准输入流 stdin 中获取数据转变为了从指定流 stream 中获取数据——该指定流可以是 stdin 也可以是指针指向的流:
int a = 0;
char arr[10] = "";
// 从标准输入流中获取数据
int finput1 = fscanf(stdin, "%d%s", &a, arr);
FILE* pf1 = fopen("C:\\Users\\LIQIAN\\Desktop\\data.txt", "rb");
if (pf1 == NULL) {
perror("pf1");
return;
}
// 从文件中获取数据
int finput2 = fscanf(pf1, "%d%s", &a, arr);
该函数可以视为 fscanf 的一个变种,从函数定义可以看到,其参数 stream 替换为了 const char* s ,我们不难推测,这代表着我们获取数据的源地址从指定流变为了指定字符串:
int a = 0;
char arr[10] = "";
char* str = "Hello World!!!";
int sinput = sscanf(str, "%d%s", &a, arr);相信大家看到这里应该明白这三个函数的具体使用方式了,这里我就不再继续展开赘述;
fprintf与 fscanf 一样,fprintf 同样是从 printf 函数变种过来。下面我们一起来看一下其函数定义:

printf 函数我相信大家已经熟悉的不能再熟悉了这里我们直接展示其用法:
int a = 0;
char arr[10] = "";
int input = scanf("%d%s", &a, arr);
printf("a = %d\tarr = %s\n", a, arr);与 scanf 不同,在 printf 中,我们不需要对占位符对应的参数进行取地址操作;

在 fprintf 函数中,新增了一个参数 stream ,意思是相比于 printf 从标准输出流 stdout 中输出数据,fprintf 则是从 stream 指定的流中输出数据——即指定流可以是 stdout 也可以是其他指定流 stream;
int a = 0;
char arr[10] = "";
// 从标准输入流中获取数据
int finput1 = fscanf(stdin, "%d%s", &a, arr);
// 从标准输出流中获取数据
fprintf(stdout, "a = %d\tarr = %s\n", a, arr);
FILE* pf1 = fopen("C:\\Users\\LIQIAN\\Desktop\\data.txt", "rb");
if (pf1 == NULL) {
perror("pf1");
return;
}
// 从文件中获取数据
int finput2 = fscanf(pf1, "%d%s", &a, arr);
// 从文件中获取数据
fprintf(pf1, "a = %d\tarr = %s\n", a, arr);
而 sprintf 与 fprintf 的区别就在于输出数据的目的地址不同:
fprintf 向直流流中输出数据sprintf 向指定字符串中输出数据 int a = 0;
char arr[10] = "";
char* str = "Hello World!!!";
char str2[20] = { 0 };
int sinput = sscanf(str, "%d%s", &a, arr);
sprintf(str2, "a = %d\tarr = %s\n", a, arr);不管是标准输入函数还是标准输出函数,其不同的函数之间的差异就是对象不同:
输入与输出实际上就是对指定对象的读取与写入:
怎么理解呢?
我们以最熟悉的 scanf/printf 说明:
scanf 函数就是从标准输入流 stdin 中读取数据,并将读取到的数据按照占位符复制到其对应的参数中;printf函数就是将占位符对应的参数的值替换到占位符中,并将 format 中的全部内容输出到标准输出流 stdout 中;这里要注意,标准输入流就是指键盘,标准输出流就是指屏幕;
因此我们可以通过键盘将数据输入到标准输入流中,再通过 scanf 对其进行读取;同时我们也可以通过 printf 函数将这些数据输出到标准输出流中,在通过屏幕将流中的内容显示出来;
今天的内容到这里就全部结束了。在本篇博客中,我们系统学习了C语言文件操作的核心读写函数,重点掌握了以下关键内容:
EOF 的整型值为 -1
如果这篇博客对您有帮助,请不吝赐予支持:
👍 点赞 - 您的认可是我持续创作的最大动力! ⭐ 关注 - 获取更多C语言编程技术的深度解析 💾 收藏 - 方便日后随时查阅,巩固知识点 🔄 转发 - 分享给更多正在学习编程的朋友,共同进步 💬 评论 - 您有任何疑问或建议,欢迎在评论区留言交流!
期待在接下来的编程学习中继续与您同行!