📚 目录
- 1.为什么要使用文件?
- 2.文件到底是什么?
- 3.二进制文件和文本文件
- 4.文件的打开与关闭
- 5.文件的顺序读写
- 6.文件的随机读写
- 7.文件缓冲区
- 8.更新文件
前言: 在 C 语言的学习旅程中,我们从变量、循环、函数等基础语法,逐步走向更贴近实际应用的编程场景,而我们在企业中多人一起工作时,文件这个概念就离我们很近了,今天我们要学习的是C语言中的文件操作,无论你是刚接触 C 语言的初学者,还是希望巩固进阶技能的开发者,相信这份内容都能为你提供清晰的指引和实用的参考。
1.为什么要使用文件?
我们是否想过我们为什么要使用文件这个操作? 一个文件工作不行吗? 答案:不行!因为在我们的企业中,多个文件一起工作会加快我们的工作效率,节约时间。 无论是保存游戏进度、记录日志信息,还是处理文件大型数据集,文件操作都是不可或缺的核心技能。它让我们的程序不再局限于单次运行的生命周期,而是能够与磁盘、网络乃至其他程序进行数据交换。如果没有,我们写的程序的数据是存储在电脑的内存中,如果程序退出,内存回收,数据就丢失了,等再次运行程序,是看不到上次程序的数据的,如果要将数据进行持久化的保存,我们可以使用文件。
🔙 返回目录
2. 文件到底是什么?
我们通常情况下谈到的文件有两种:程序文件、数据文件。 程序文件:C语言中程序文件源程文件(后缀为.c形式),目标文件(windows中后缀为.obj)和可执行程序(windows中后缀为.exe)。 数据文件:程序运行中读取的数据,比如我们在进行文件操作的时候进行的读写和输出文件内容的形式。 文件名字:方便用户的使用和识别。 文件名字包含3部分:文件路径+文件名主干+文件后缀。
🔙 返回目录
3. 二进制文件和文本文件
数据文件被分为:二进制文件和文本文件。 二进制文件:数据在内存中以二进制的形式存储,不转化二进制的形式输出到外存文件中。 文本文件:以ASCLL字符存储的文件。
字符统一使用ASCLL值进行存储,数值型数据二进制和ASCLL形式存储都行。 如有整数10000,如果以ASCII码的形式输出到磁盘,则磁盘中占用5个字节(每个字符⼀个字节),而⼆进制形式输出,则在磁盘上只占4个字节。 
#include<tdio.h>
int main()
{
int a = 10000;
FILE* pf = fopen("test.txt", "wb");
fwrite(&a, 4, 1, pf);//⼆进制的形式写到⽂件中
fclose(pf);//关闭文件
pf = NULL;//制空
return 0;
}

🔙 返回目录
4. 文件的打开与关闭

在我们进行文件的打开和操作的时候,流程按上图所示。
标和标准流
什么是流? 流:我们程序的数据需要输出到各种外部设备,也需要从外部设备获取数据,不同的外部设备的输⼊输出操作各不相同,为了方便程序员对各种设备进行方便的操作,我们抽象出了流的概念,我们可以把流想象成流淌着字符的河。 标准流: 我们C语言在编写程序的时候,也就是在启动我们C语言程序的时候,默认会打开三个流:stdin、stdout、stderr。 stdin:标准输出流,多数环境下从我们键盘中读取数据,比如scanf这个函数。 stdout:标准输出流,我们多数环境下,输出到我们电脑屏幕上(也就是显示器),如printf这个函数。 stderr:标准错误流,将我们的错误输出到显示器上面。 C语言给我们打开的三个流的类型都是:FILE*类型(文件指针类型)。
文件指针
每个被使用的文件都在内存中开辟了一个文件信息区域,这个区域用来存放文件的相关信息,而这些信息都是保存在结构体变量中,结构体变量由系统声明,取名为:FILE 比如我们VS2022中的文件指针结构体的声明:
这只是我们Vs2022中文件指针的声明,每个编译器的声明可能不同。 每当我们创建一个文件指针类型的时候系统会根据我们创建的文件指针类型创建一个文件指针变量,通过FILE来维护我们这个文件指针变量,后续时候给我们提供便捷的操作方法。
我们建立一个文件指针。
#include<stdio.h>
int main()
{
FILE* pf = NULL;
return 0;
}
当我们文件指针访问的不是NULL的时候,可以使pf指向我们要指向文件的文件信息区,可以间接找相关联的文件。
如何打开和关闭文件?
打开文件:
功能:fopen这个函数用来打开const char * filename(指定文件)同时打开一个相关联的流,后续的文件维护和操作都是通过FILE进行操作的。 const char *filename:打开的文件名字。 mode:对文件进行的操作。 返回值: 成功: 返回指向FILE对象的指针 失败: 返回NULL,打开文件的时候要进行判断文件是否打开。
#include<stdio.h>
int main()
{
FILE* pf = fopen("test.tst", "r");
if (pf == NULL)
{printf("打开失败\\n");
perror("pf\\n");
}
//用完文件的时候需要关闭文件
fclose(pf);
return 0;
}
操作方式: 
关闭文件:
功能:关闭我们指定的文件(FILE * stream) FILE * stream:关闭的文件名字。 返回值: 成功: 返回0 失败: 返回EOF
🔙 返回目录
5. 文件的顺序读写
文件读写涉及到的函数: 
fputc:
功能:将指定的字符写入stream指向的输出流中,通常用于文件或者标准输出流写入字符。 character:写入的字符 stream:文件指针类型指向了输出流(正常来讲为stdout) 返回值: 成功: 返回写入的字符(int 类型形式) 失败: 返回EOF
#include<stdio.h>
int main()
{
FILE* pf = fopen("test.tst", "w");
if (pf == NULL)
{printf("打开失败\\n");
perror("pf\\n");
}
//用完文件的时候需要关闭文件
fputc('a', pf);
fputc('c', pf);
fclose(pf);
pf = NULL;//防止成为野指针
return 0;
}

🔙 返回目录
fgetc
功能:从stream指向的流中读取一个字符。读取完这个字符后会进行向后移动一个字符,下回读取的时候对那个字符读取。 stream:FILE*类型的文件指针,可以是我们的标准流stdin,也可以是别的文件指针流。 返回值: 成功: 返回读取的字符(int类型形式) 失败: EOF
#include<stdio.h>
int main()
{
FILE* pf = fopen("test.tst", "r");
if (pf == NULL)
{printf("打开失败\\n");
perror("pf\\n");
}
int r = fgetc(pf);
printf("%d\\n", r);
printf("%c\\n", r);
//用完文件的时候需要关闭文件
fclose(pf);
pf = NULL;//防止成为野指针
return 0;
}

feof和ferror
功能:检测是否遇到文件末尾,遇到文件末尾,文件读取就会结束。如果feof检测到文件结束指示符已经被设置,则返回非0,反之则设置为0。
🔙 返回目录
如果文件读/写文件过程中发生了读取错误,可以用ferror来检测。如果ferror检测到文件错误指示符已经被设置,则返回非0,反之则设置为0。
#include<stdio.h>
int main()
{
FILE* pf = fopen("test.tst", "r");
if (pf == NULL)
{printf("打开失败\\n");
perror("\\n");
}
int i = 0;
for (i = 0; i < 10; i++)
{
int c = fgetc(pf);
if (c == EOF)
{
if (feof(pf))
{
printf("遇到文件末尾\\n");
}
else if(ferror(pf))
{
printf("读取错误\\n");
}
}
else
{
fputc(c,stdout);
}
}
//用完文件的时候需要关闭文件
fclose(pf);
pf = NULL;
return 0;
}

#include<stdio.h>
int main()
{
FILE* pf = fopen("test.tst", "w");
if (pf == NULL)
{printf("打开失败\\n");
perror("\\n");
}
int i = 0;
for (i = 0; i < 10; i++)
{
int c = fgetc(pf);
if (c == EOF)
{
if (feof(pf))
{
printf("遇到文件末尾\\n");
}
else if(ferror(pf))
{
printf("读取错误\\n");
}
}
else
{
fputc(c,stdout);
}
}
//用完文件的时候需要关闭文件
fclose(pf);
pf = NULL;
return 0;
}

🔙 返回目录
fputs
功能:讲str指向的字符串写入到参数stream指定的流中(不包含\\0) str:是指针,指向了我们要写的字符串。 注意: 字符串必须以\\0结束。
stream:是一个FILE*的指针。指向了要写入的字符串流。 返回值: 成功: 返回非负整数 失败: 返回EOF,同时可以用fperror来检测错误原因。
#include<stdio.h>
int main()
{
FILE* pf = fopen("test.tst", "w");
if (pf == NULL)
{
printf("打开失败\\n");
perror("\\n");
}
fputs("abcd", pf);
//用完文件的时候需要关闭文件
fclose(pf);
pf = NULL;
return 0;
}

🔙 返回目录
fgets
功能:从stream指定的输出流中读取字符串,然后储存到str指定的空间中。(读取到换行、文件末尾、\\0、指定字符) str:指向字符串的指针,存储字符串的地址。 stream:输入到文件指针
#include<stdio.h>
int main()
{
FILE* pf = fopen("test.tst", "r");
if (pf == NULL)
{printf("打开失败\\n");
perror("\\n");
}
char arr[10] = { 0 };
fgets(arr, 5, pf);
printf("%s\\n", arr);
//用完文件的时候需要关闭文件
fclose(pf);
pf = NULL;
return 0;
}

🔙 返回目录
fprintf
功能:将格式化的数据写入到指定的文件流中的函数。与printf类似,fprintf可以控制任意的输出流。 stream:指向FILE*对象的指针,表示要写入的文件流。 format:格式化字符串,包含要写入的文本和格式化说明符号。 … :可变参数,提供与格式化字符串中说明对应的数据。 返回值: 成功: 返回写入字符串总数。 失败: 先设置对应的错误指示器,再返回一个负数。
#include<stdio.h>
struct student
{
char name[10];
int age;
};
int main()
{
FILE* pf = fopen("test.tst", "w");
if (pf == NULL)
{printf("打开失败\\n");
perror("\\n");
}
struct student stu = { "zhangsan",20};
fprintf(pf, "%s,%d", stu.name, stu.age);
//用完文件的时候需要关闭文件
fclose(pf);
pf = NULL;
return 0;
}

🔙 返回目录
fscanf
功能:从指定文件流中读取格式化数据的函数,类似于scanf,但并没有显示控制台输入。 stream:指向FILE*对象的指针,表示要读取的文件流。 format:格式化字符串,定义如何解析输入数据。如(%d、%f等)
… :可变参数列表,提供存储数据的变量地址。 返回值: 成功: 函数返回成功填充到参数列表中的项数。该值可能与预期项数⼀致,也可能因以下原因少于预期(甚至为零)
- 格式和数据匹配失败
- 读取发生错误
- 到达文件末尾
如果再成功读取任何数据之前发生:
- 发生读取错误,会在对应流上设置错误提示符
- 到达文件末尾,会在对应流上设置文件结束提示符。
#include<stdio.h>
struct Stu
{
char name[30];
int age;
float score;
};
int main()
{
struct Stu s = { 0 };
FILE* pf = fopen("test.tst", "r");//如果我特意把文件名写错就会发生下面的情况。
if(pf == NULL)
{
perror("fopen\\n");
return 1;
}
fscanf(pf, "%s %d %f", s.name, &(s.age), &(s.score));
fprintf(stdout,"%s %d %.1f\\n", s.name, s.age, s.score);
fclose(pf);
pf = NULL;
return 0;
}
错误的: 
正确的: 
🔙 返回目录
fwrite
功能:函数用于数据块写入stream指向的文件流中,二进制的形式存在。 ptr:指向要写入的数据块的指针。 size:写入的每个数据项大小。 count:要写入的数据项数量。 stream:指向FILE类型结构体的指针,指定了写入数据的文件流。 返回值: 返回实际写入的数据项数量。如果发生错误返回值可能小于count。
#include <stdio.h>
int main()
{
int data[] = {1, 2, 3, 4, 5};
//
打开⽂件
FILE *fp = fopen("data.bin", "wb");
// 检查⽂件是否成功打开
if (fp == NULL) {
perror("fopen");
return –1;
}
// 将数据写⼊⽂件
// 关闭⽂件
if (fwrite(data, sizeof(int), 5, fp) != 5)
{
perror("fwrite");
return –1;
}
fclose(fp);
fp = NULL;
return 0;
}
🔙 返回目录
fread
功能:从 stream指向文件流中读取数据块,并将其存储到ptr 指向的内存缓冲区中。 ptr:指向内存区域的指针,用于存储文件中读取的数据。 size:读取的数据大小。 count:要读取数据块的数量。 stream:指向FILE类型的结构体指针,指定了要从中读取数据的文件流。 返回值: 返回实际读取的数据块的数量。 注意:
- 需要包含<stdio.h>头文件
- 使用前需要确保文件用二进制可读的方式打开
- ptr指向的内存必须足够大
- 对于文本文件的读取通常使用fgetc和fsacnf这两个函数。
#include<stdio.h>
int main()
{
int data[5] = { 0 };
FILE * fp = fopen("test.tst", "rb");
if (fp == NULL) {
perror("fopen");
return –1;
}
size_t num_read = fread(data, sizeof(int), 5, fp);
if (num_read != 5)
{
if (feof(fp))
printf("Reached end of file\\n");
else if (ferror(fp))
printf("Error reading file\\n");
}
else
{
for (int i = 0; i < 5; i++)
{
printf("Data[%d]: %d\\n", i, data[i]);
}
}
fclose(fp);
return 0;
}
🔙 返回目录
sscanf
功能:从字符串中读取格式化数据。常用于解析字符串中的结构化数据(如提取数字、分割文本等) str:要解析的源字符串。 format:格式化字符串,定义如何解析数据。 …:可变参数列表。 返回值: 成功: 返回成功解析并赋值的参数数量(非负值)。 失败: 返回EOF
#include <stdio.h>
struct Stu
{
char name[30];
int age;
float score;
};
int main()
{
char str[] = "zhangsan 100 85.5";
struct Stu s = { 0 };
sscanf(str, "%s %d %f", s.name, &(s.age), &(s.score));
printf("%s %d %.1f\\n", s.name, s.age, s.score);
return 0;
}

🔙 返回目录
6. 文件的随机读写
fseek

功能:根据⽂件指针的位置和偏移量来定位文件指针(文件内容的光标) stream:指向FILE类型结构体的指针,指定了要定位文件指针的文件流。 offset:偏移量。 origin:偏移的起点。
- SEEK_SET:文件开始位置
- SEEK——CUR:文件指针当前位置。
- SEEK_END:文件末尾位置。 返回值: 成功: 如果文件位置指针被成功移动,返回0 失败: 返回-1 ⼆进制文件vs文本文件: 对于二进制文件fseek能准确移动到任何位置。 对于文本文件,fseek的行为在某些环境下可能不确定。
#include <stdio.h>
int main()
{
FILE* pFile;
pFile = fopen("example.txt", "wb");
fputs("This is an apple.", pFile);
fseek(pFile, 9, SEEK_SET);
fputs(" sam", pFile);
fclose(pFile);
return 0;
}
🔙 返回目录
ftell

功能:返回文件指针相对于起始位置的偏移量 stream:指向一个已经打开的文件指针。 返回值: 成功: 返回当前文件位置指针相对于文件开头的偏移量 失败: 返回EOF ⼆进制文件vs文本文件: 对于二进制文件,ftell的返回值就是相对于文件开头的精确字节数 对于文本文件,由于不同系统对换行符的处理不同ftell 的返回值可能不直接等于从⽂件开头计数的字符数
🔙 返回目录
rewind
功能:让文件指针的位置回到文件的起始位置。 stream:指向一个已经打开的文件指针。 注意: rewind会清楚文件的错误标识和文件结束标志。 这意味着即使在读取文件时遇到了错误或到达了文件末尾,可以用rewind重新进行操作。
#include <stdio.h>
int main()
{
int n;
FILE* pFile;
char buffer[27];
pFile = fopen("myfile.txt", "w+");
for (n = 'A'; n <= 'Z'; n++)
fputc(n, pFile);
rewind(pFile);
fread(buffer, 1, 26, pFile);
fclose(pFile);
buffer[26] = '\\0';
printf(buffer);
return 0;
}

🔙 返回目录
7. 文件缓冲区
什么时文件缓冲区?
ANSI C标准采用“缓冲文件系统”处理数据⽂件的,所谓缓冲文件系统是指系统自动地在内存中为程序中每⼀个正在使用的文件开辟⼀块“文件缓冲区”。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才⼀起送到磁盘上。如果从磁盘向计算机读⼊数据,则从磁盘⽂件中读取数据输⼊到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的⼤⼩根据C编译系统决定的。 
fflush

功能:强制刷新缓冲区 参数为NULL时:刷新所有的输出流 stream:指向文件流的指针 返回值:成功返回0;失败返回EOF
#include<stdio.h>
#include<Windows.h>
int main()
{
FILE* pf = fopen("test.tst", "w");
fputs("abcdef", pf);
Sleep(1000);
printf("刷新\\n");
fflush(pf);
//关闭文件;
fclose(pf);
pf = NULL;
return 0;
}
结论: 因为有缓冲区的存在,C语言在操作文件的时候,需要做刷新缓冲区或者在文件操作结束的时候关闭文件。 如果不做,可能导致读写文件的问题
🔙 返回目录
8. 更新文件
注意: 使用之前需要用fflush刷新文件缓冲区。 读完文件的时候,需要再次写文件的时候,在使用之前序要fseek,rewind重新定位。
#include <stdio.h>
int main()
{
FILE* fp = fopen("test.tst", "w+");
if (fp == NULL)
{
perror("fopen for w+");
return 1;
}
fputs("abcdefghi", fp);//写入
fflush(fp);
//要读取字符b的位置需要定位
fseek(fp, 1, SEEK_SET);
int ch = fgetc(fp);
printf("%c\\n", ch);
fseek(fp, –1, SEEK_CUR);
fputs("hello", fp);
fclose(fp);
fp = NULL;
return 0;
}

🔙 返回目录




