嗨(❁´◡`❁)✲゚,我们已经学习C语言的许多功能,今天我们来学习一些不一样的——用C语言对文件进行操作的重要知识点,让我们来该共同学习吧

概念:磁盘(硬盘)上的文件是文件。
如果没有文件,我们写的程序的数据是存储在电脑的内存中,如果程序退出,内存回收,数据就丢失了,等再次运行程序,是看不到上次程序的数据的,如果要将数据进行持久化的保存,我们可以使用文件。 就像我们对文件进行操作,下次我们再进行查看时文件会保留我们的修改,而我们对程序中的的值进行scanf赋值,下一次运行却不是我们上次赋的值
程序设计中,我们⼀般谈的文件有两种:程序文件、数据文件(从文件功能的角度来分类的)。
程序文件包括源程序文件(后缀为.c),目标文件(windows环境后为.obj),可执行程序(windows环境后缀为.exe)。
文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件。 我们本篇讨论的是数据文件 在以前各章所处理数据的输入输出都是以终端为对象的,即从终端的键盘输入数据,运行结果显示到显示器上。 其实有时候我们会把信息输出到磁盘上,当需要的时候再从磁盘上把数据读取到内存中使用,这里处理的就是磁盘上文件。
一个文件要有唯一的文件标识,以便用户识别和引用。 文件名包含三部分:文件路径+文件名主干+文件名后缀 比如,C:\Users\lenovo\Desktop就是一个文件名。 为了方便起见,文件标识常被称为文件名。

根据数据的组织形式,数据文件分为二进制文件和文本文件。
二进制文件:数据在内存中以二进制的形式存储,如果不加转换地输出到外存(磁盘)的文件中文本文件:如果要求在外存上以ASCLL码的形式存储,则需要在存储前转换,以ASCLL字符的形式存储的文件字符一律以ASCll形式进行存储,数值类型既可以用ASCll形式进行存储,也可以使用二进制进行存储 比如整型10000进行存储, 如果以ASCII码的形式输出到磁盘,则磁盘中占用5个字节(每个字符一个字节), 而二进制形式输出,则在磁盘上只占4个字节。

这里进行操作,不必理解为什么这样及函数的作用,这里只是对文件进行操作以2进制存储,重点在2进制文件,后面会讲实现
#include<stdio.h>
int main()
{
int a= 10000;
FILE* pf = fopen("tes.txt", "wb");//打开文件
fwrite(&a, 4, 1, pf);//存储
fclose(pf);//关闭
pf = NULL;//为空指针,避免成为野指针
return 0;
}这里出现我们创建的文件

直接打开,发现为乱码,这是因为我们以2进制的形式进行存储,记事本以字符的形式进行存储,无法识别

这时我们通过VS进行打开

结果如下,我们发现就是就是2进制,我们转换为10进制后就是为10000

在进行文件的打开和关闭前我们需要先了解流和标准流的概念
概念:我们程序的数据需要输出到各种外部设备,也需要从外部设备获取数据,不同的外部设备的输入输出操作各不相同,为了方便程序员对各种设备进行方便的操作,我们抽象出了流的概念,我们可以把流想象成流淌着字符的河。
C程序针对文件、画面、键盘等的数据输入输出操作都是通过流操作的。 一般情况下,我们要想向流里写数据,或者从流中读取数据,都是要打开流,然后操作。
那为什么我们在键盘上输入数据,向屏幕上输出数据,却没有打开流呢? 这是因为C语言程序在启动的时候就默认打开了3个流,分别是:
标准流名称 | 描述 | 关联函数 | 目标设备 |
|---|---|---|---|
stdin | 标准输入流,用于读取用户输入 | scanf | 键盘 |
stdout | 标准输出流,用于输出程序信息 | printf | 显示器 |
stderr | 标准错误流,用于输出错误/异常信息 | -(可配合 fprintf 使用) | 显示器 |
stdin 是程序的“输入通道”,默认从键盘读取数据(如 scanf 就是从 stdin 中获取输入);stdout 是程序的“正常输出通道”,默认将信息打印到显示器(如 printf 的内容会输出到 stdout);stderr 是程序的“错误输出通道”,专门用于输出错误、异常信息(例如程序崩溃提示、逻辑错误提示等),和 stdout 一样默认输出到显示器,但在实际场景中(如日志重定向)可与 stdout 区分处理。这是默认打开了这三个流,我们使用scanf、printf等函数就可以直接进行输入输出操作的。 stdin、stdout、.stderr三个流的类型是:FILE*,通常称为文件指针。 C语言中,就是通过FILE*的文件指针来维护流的各种操作的。
缓冲文件系统中,关键的概念是“文件类型指针”,简称“文件指针”。 每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件的名字,文件状态及文件当前的位置等)。这些信息是保存在一个结构体变量中的。该结构体类型是由系统声明的,取名FILE.
struct _iobuf {
char* _ptr;
int _cnt;
char* _base;
int _flag;
int _file;
int _charbuf;
int _bufsiz;
char* _tmpfname;
};
typedef struct _iobuf FILE;不同的C编译器的FLE类型包含的内容不完全相同,但是大同小异。 每当打开一个文件的时候,系统会根据文件的情况自动创建一个FLE结构的变量,并填充其中的信息,使用者不必关心细节。 一般都是通过一个FLE的指针来维护这个FLE结构的变量,这样使用起来更加方便。 下面我们可以创建一个FLE*的指针变量
FILE* pf; //文件指针变量定义pf是一个指向FLE类型数据的指针变量。可以使指向某个文件的文件信息区(是一个结构体变量)。通过该文件信息区中的信息就能够访问该文件。也就是说,通过文件指针变量能够间接找到与它关联的文件。比如:

我们通常对文件进行的操作:打开文件->读/写文件->关闭文件
编写程序时打开文件,都会返回应该FILE*的指针变量指向该文件,相当于建立了指针和文件的关系。
使用fopen函数打开文件,fclose来关闭文件
//打开文件
FILE * fopen ( const char * filename, const char * mode );//filename文件名,mode 操作
//关闭文件
int fclose ( FILE * stream );(1)函数功能: 打开在参数 filename 中指定其名称的文件,并将其与流相关联,该流可以在将来的作中通过返回的 FILE 指针进行标识。 流上允许的作及其执行方式由 mode 参数定义。 (2)参数: filename:表示被打开的文件的名字,这个名字可以是绝对路径,也可以是相对路径;(路径下面会专门讲) mode表示文件的打开方式,文件的打开模式在下面会有展示 (3)返回值 如果成功打开文件,该函数将返回指向 FILE 对象的指针,该对象可用于在将来的作中标识流。 否则,将返回空指针。
mode文件的打开方式
文件使用方式 | 含义 | 如果指定文件不存在 |
|---|---|---|
“r”(只读) | 为了输入数据,打开一个已经存在的文本文件 | 出错 |
“w”(只写) | 为了输出数据,打开一个文本文件 | 建立一个新的文件 |
“a”(追加) | 向文本文件尾添加数据 | 建立一个新的文件 |
“rb”(只读) | 为了输入数据,打开一个二进制文件 | 出错 |
“wb”(只写) | 为了输出数据,打开一个二进制文件 | 建立一个新的文件 |
“ab”(追加) | 向一个二进制文件尾添加数据 | 建立一个新的文件 |
“r+”(读写) | 为了读和写,打开一个文本文件 | 出错 |
“w+”(读写) | 为了读和写,建立一个新的文件 | 建立一个新的文件 |
“a+”(读写) | 打开一个文件,在文件尾进行读写 | 建立一个新的文件 |
“rb+”(读写) | 为了读和写打开一个二进制文件 | 出错 |
“wb+”(读写) | 为了读和写,新建一个新的二进制文件 | 建立一个新的文件 |
“ab+”(读写) | 打开一个二进制文件,在文件尾进行读和写 | 建立一个新的文件 |
代码
#include <stdio.h>
int main ()
{
FILE * pFile;
//打开文件
pFile = fopen ("text.text","w");
if(pF==NULL)
{
perror("fopen");
return 1;
}
return 0;
}这里我们对pFile = fopen (“text.text”,“w”);中文件的地址进行讲解
'.'当前路径
'..'表示上一路径上一路径就是下图箭头所指

展示如下 当前路径
pFile = fopen("text.txt", "w");上一路径
pFile = fopen("./../text.txt", "w");同理,前一路径
pFile = fopen("./../../text.txt", "w");但是防止出现转义字符影响,所以我们用2个//防止转义 如下
pFile = fopen("text.txt", "w");//当前
pFile = fopen(".//..//text.txt", "w");//上一路径
pFile = fopen(".//..//..//text.txt", "w");//前一路径
C:\Users\lenovo\Desktop就是它的绝对路径 操作为 这里还是把/改为//来防止转义字符
pFile = fopen("C:\\Users\\lenovo\\Desktop", "w");有打开就会有关闭,关闭文件函数为fclose
参数:指向指定要关闭的流的 FILE 对象的指针。 功能:关闭文件 关闭与流关联的文件并取消关联。与流关联的所有内部缓冲区都将与流取消关联并刷新:写入任何未写入输出缓冲区的内容,并丢弃任何未读输入缓冲区的内容。 返回值: 如果成功关闭流,则返回零值。失败 时,返回 EOF。
代码
fclose(pf);//关闭
pf = NULL;//为空指针,避免成为野指针完整代码
#include<stdio.h>
int main()
{
int a= 10000;
FILE* pf = fopen("tes.txt", "wb");//打开文件
//操作
//关闭文件
fclose(pf);
pf = NULL;//为空指针,避免成为野指针
return 0;
}函数名 | 功能 | 适用于 |
|---|---|---|
fgetc | 字符输入函数 | 所有输入流 |
fputc | 字符输出函数 | 所有输出流 |
fgets | 文本行输入函数 | 所有输入流 |
fputs | 文本行输出函数 | 所有输出流 |
fscanf | 格式化输入函数 | 所有输入流 |
fprintf | 格式化输出函数 | 所有输出流 |
fread | 二进制输入 | 文件输入流 |
fwrite | 二进制输出 | 文件输出流 |
上面说的适用于所有输入流一般指适用于标准输入流(键盘)和其他输入流(如其他输入流);所有输出流一般指适用于标准输出流(屏幕/显示器)或其他输出流(如文件输出流)。

就是将一个字符以ASCll码值(或对应编码值)以2进制的形式存储到文件中 代码如下,如果想把26个字母全部存储,可以用循环操作
#include<stdio.h>
//打开文件
int main()
{
FILE* pf = fopen("haha.txt", "w");
if (pf == NULL)
{
perror("fopen");
pf = NULL;
}
//写文件
fputc('a', pf);
fputc('b', pf);
fputc('c', pf);
fputc('d', pf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}我们也可以利用我们前面讲的标准流stdout直接输出
#include<stdio.h>
int main()
{
fputc('a', stdout);
fputc('b', stdout);
fputc('c', stdout);
fputc('d', stdout);
return 0;
}结果

而这样以stdout直接输出到屏幕上的话,fputc这个函数就与putchar功能一样啦 代码如下:
#include<stdio.h>
int main()
{
fputc('a', stdout);
fputc('b', stdout);
fputc('c', stdout);
fputc('d', stdout);
putchar('a');
putchar('b');
putchar('c');
putchar('d');
return 0;
}结果如下:的确一样


简单来说就是读取文件中的一个字符,我们前面已经向文件中写了abcd,我们现在进行读取,因为一次只能读取1个字符,所以我们用循环来读取 只读一个字符
#include<stdio.h>
int main()
{
FILE* pf = fopen("haha.txt", "r");
if (pf == NULL)
{
perror("fopen"); // 打开失败时打印错误信息
return 1; // 直接退出,避免后续错误操作
}
// 读文件(此时pf一定非NULL,无需再次判断)
int ch = fgetc(pf);
printf("%c", ch);
// 关闭文件(必须在pf有效时调用)
fclose(pf);
pf = NULL; // 关闭后置空,避免野指针
return 0;
}用循环读取完,因为读取1个字符光标会移向下一个
#include<stdio.h>
int main()
{
FILE* pf = fopen("haha.txt", "r");
if (pf == NULL)
{
perror("fopen"); // 打开失败,打印错误信息
return 1; // 直接退出,无需后续操作
}
// 读文件(此时pf一定非NULL)
int ch;
while ((ch = fgetc(pf)) != EOF)
{
printf("%c", ch);
}
// 关闭文件(必须在文件操作完成后,且pf有效时调用)
fclose(pf);
pf = NULL; // 置空避免野指针
return 0;
}结果

我们可以把1和2中结合一下,fgetc读取,fputc输出
#include <stdio.h>
int main()
{
int ch;
while ((ch = fgetc(stdin)) != EOF)
{ // 从键盘(stdin)读1字符
fputc(ch, stdout); // 立即输出到屏幕(stdout)
}
return 0;
}
这个与fputc的区别就是fputc只能存储1个字符,而fputs可以直接把字符串存储到文件中 代码如下:
#include<stdio.h>
//打开文件
int main()
{
FILE* pf = fopen("haha.txt", "w");
if (pf == NULL)
{
perror("fopen");
pf = NULL;
}
//写文件
fputs("abcd", pf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}同理,用stdout可以打印在屏幕上
#include<stdio.h>
int main()
{
fputs("abcd", stdout);
return 0;
}结果

这个是我最常用的函数,可以直接读取一行,不用担心会因为空格跳过

>char * fgets ( char * str, int num, FILE * stream );就是把stream中的num-1个元素,放在str中 这个函数就是将stream中读取num-1个元素和一个\0到str中
我们来使用它来将文件中的字母转换到数组中
#include<stdio.h>
int main()
{//打开文件
FILE* pf = fopen("haha.txt", "r");
if (pf == NULL)
{
perror("fopen");
pf = NULL;
}
//写文件
char arr[20] = "xxxxxxxxxxxxxx";
fgets(arr, 10, pf);
printf("%s", arr);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}我们从下图可看出来的确会读取num-1个元素和/0,但是注意如果文件中含有/n且缓存区足够长时会读到换行符\n

我们可以把fgets应用到我们平常输入字符串中
#include<stdio.h>
#include<string.h>
int main()
{
char arr[1000];
fgets(arr, 100, stdin);
arr[strcspn(arr, "\n")] = '\0';
int num;
scanf("%d", &num);
printf("%s", arr);
return 0;
}其中 arr[strcspn(arr, “\n”)] = ‘\0’;是防止fgets最后\n直接影响到下一个读取,比如如果没有会影响num读取直接读取\n使读取失败,也可以用getchar()吸收也可以

我们发现如果这样不输出,原因是fgets会读取一行,所以我们输入后回车,就可以啦



int fscanf ( FILE * stream, const char * format, … );
功能
核心是 格式化读取数据,与 fprintf 成对使用,按指定格式从输入流(键盘/文件)解析并读取多类型数据(int、float、字符串等),支持自定义分隔符,无需手动处理数据格式转换,适用于需按固定格式读取数据的场景(如配置文件、结构化文本)。
参数
函数原型:
int fscanf(FILE *stream, const char *format, ...);stream指向FILE对象的指针,表示要读取的文件流(如stdin 、文件指针等)
format :格式化字符串,定义如何解析输入数据(如%d 、 %f 、 %s 等)。
... :可变参数列表,提供存储数据的变量地址(需与格式字符串中的说明符匹配)。
下面我们以代码展示 读取文件
#include<stdio.h>
struct Stu
{
char name[20];
int age;
float score;
};
int main()
{
struct Stu s = { 0 };
FILE* pf = fopen("haha.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读文件
fscanf(pf,"%s %d %f", s.name, &(s.age), &(s.score));//如果想多次取可用while,因为我前面只存储了字符,所以只能读取字符,后面全为0
////读文件
//while (fscanf(pf, "%s %d %f", s.name, &(s.age), &(s.score)) != EOF)
//{
// printf("%s %d %f\n", s.name, s.age, s.score);
//}
printf("%s %d %f\n",s.name,s.age, s.score);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}因为我前面只存储了字符abcd,所以只能读取字符,后面全为0

读取键盘输入
#include <stdio.h>
int main()
{
int num;
char str[50];
float score;
// 按 "整数 字符串 浮点数" 格式读取键盘输入
printf("输入格式:数字 字符串 成绩(如 10 小明 95.5):\n");
fscanf(stdin, "%d %s %f", &num, str, &score); // 变量地址:int/float加&,数组名本身是地址
printf("结果:编号=%d,姓名=%s,成绩=%.1f\n", num, str, score);
return 0;
}

int fprintf ( FILE * stream, const char * format, … );
fprintf是将格式化数据写入指定文件流的函数,和fscanf成对使用,核心是按照指定格式(如%d %s %f)将数据写入目标(文件/屏幕),对应fprintf(文件指针,格式控制符,输入数据)
我们可以发现这个参数前两个与fscanf一样,最后一个就是变量这里就不再讲了,
我们可以用它来写进文件
代码如下
#include<stdio.h>
struct Stu
{
char name[20];
int age;
float score;
};
int main()
{
struct Stu s = { "hh",20,80.5f };
FILE* pf = fopen("haha.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//写文件
fprintf(pf, "%s %d %f", s.name, s.age, s.score);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}如果在进行完这个程序再用fscanf进行读取,就可以看出效果啦

同理,printf第一个参数改为stdout后fprintf(stdout, , ) 效果与printf效果一样

功能: fwrite是2进制写入函数,核心将“数据块”批量写入(如结构体、数组),不做格式转换,效率比fprintf(文本格式化)高 参数:
size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream )ptr:指向要写入的数据块的地址;
size:要写入的每个数据项的大小(以字节为单位);
count:要写入的数据项的数量;
stream:指向FILE类型结构体的指针,指向了要写入数据的文件流;
返回值
返回成功写入的元素个数
if返回值 =count——正确写入 <count——写入出错(磁盘空间不足、文件权限不足) =0——要么数据地址为NULL,要么文件指针无效
我们这里写一个结构体数组到二进制文件中 代码如下:
#include<stdio.h>
struct Student
{ int id;
char name[20];
float score;
}; // 自定义结构体
int main()
{
FILE* fp = fopen("student.bin", "wb"); // 二进制写模式(wb)
struct Student stu[2] = { {1, "张三", 88.5}, {2, "李四", 92.0} };
if (!fp)
return 1;
// 写入2个Student结构体,每个大小sizeof(struct Student)
fwrite(stu, sizeof(struct Student), 2, fp);
fclose(fp);
fp = NULL;
return 0;
}文件以2进制形式打开


功能: 核心是 二进制块读取,与 fwrite 成对使用,按指定“数据块大小+个数”批量读取二进制文件(如结构体、数组),不做格式转换,直接将文件原始字节数据存入指定缓冲区,效率高于格式化读取(如 fscanf ),适用于批量结构化数据(数组、结构体)的读取。 参数
size_t fread(void* ptr, size_t size, size_t count, FILE* stream);ptr:接收读取数据的首元素的地址(存储数据的目标位置);
size:单一元素的字节数
count:读取的元素总数(数据块个数)
stream:指向FILE类型结构体的指针,指定了要从中读取数据的文件流。(注意:要以2进制打开(rb));
返回值
返回成功读取的元素个数
if返回值 1、<count——读到文件末尾(EOF)或读取时出错(如文件损坏) 2、=count——正常读取 3、=0——要么已到达文件末尾,要么缓冲区地址为NULL或文件指针无效
我们前一个函数fwrite已经已2进制存储啦,现在来读取 代码如下
#include <stdio.h>
struct Student
{ int id;
char name[20];
float score;
};
int main()
{
FILE* fp = fopen("student.bin", "rb"); // 二进制读模式(rb)
struct Student stu[2]; // 存储读取的数据
size_t readNum; // 接收成功读取的元素个数
if (!fp)
return 1;
// 读取2个Student结构体,存入stu数组
readNum = fread(stu, sizeof(struct Student), 2, fp);
// 验证读取结果
printf("成功读取 %zu 个学生数据:\n", readNum);
for (int i = 0; i < readNum; i++)
{
printf("ID:%d,姓名:%s,成绩:%.1f\n", stu[i].id, stu[i].name, stu[i].score);
}
fclose(fp);
return 0;
}结果如下,和我们输入的一样

上面讲了有些函数存储时格式化或不格式化,担心大家不懂,现在列个表格进行对比 格式化与不格式化数据场景对比表
对比维度 | 格式化数据 | 不格式化数据(原生数据) |
|---|---|---|
核心特征 | 遵循固定规则/结构存储/传输 | 以原生字节流形式存储/传输,无预设结构 |
数据结构 | 有明确结构(如键值对、字段分隔、格式符约束) | 无统一结构,仅为连续字节/纯文本流 |
人类可读性 | 高(直接看懂内容,如表格、JSON文本) | 低(二进制需解析,纯文本无分隔也难理解) |
程序解析难度 | 低(按固定格式直接解析,无需额外逻辑) | 高(需自定义解析规则,如二进制文件解析协议) |
典型场景 | 配置文件、接口数据交换、日志记录、用户输入输出 | 媒体文件(图片/视频)、二进制文件存储、高效数据传输 |
常见示例 | JSON文件、CSV表格、printf("%d", num)输出、数据库表 | 图片(.png/.jpg)、视频(.mp4)、二进制日志、fread/fwrite读写的文件 |
优点 | 易调试、跨平台/跨程序兼容、开发效率高 | 存储/传输效率高(占用空间小、速度快)、保留原始数据细节 |
缺点 | 额外占用少量存储空间、传输有格式解析开销 | 可读性差、调试困难、跨程序需统一解析规则 |
我们接下来来对比
scanf/fscanf/sscanf printf/fprintf/sprintf
我们先来认识一下sscanf和sprintf

功能: **从指定字符串中按格式化规则读取数据,并存储到对应变量中。**常用于解析字符串中的结构化数据(如从配置字符串提取参数、解析协议字符串等场景)。 参数
int sscanf ( const char * s, const char * format, ...);const char *str:待解析的源字符串(const修饰,保证字符串不被修改)。
const char *format:格式控制字符串(与scanf格式符一致,如%d、%s、%f等)。
可变参数列表:接收数据的变量地址(如&num、&str等),需与format中的格式符一一对应。
#include <stdio.h>
#include <string.h>
int main() {
// 待解析的源字符串(包含姓名和年龄)
char str[] = "name:Alice age:22";
char name[20];
int age;
// 用sscanf按格式提取数据
sscanf(str, "name:%s age:%d", name, &age);
// 打印结果
printf("姓名:%s,年龄:%d\n", name, age);
return 0;
}结果


char *str:目标字符数组(需保证足够大的空间,否则易引发缓冲区溢出风险)。const char *format:格式控制字符串(与printf格式符一致)。format中的格式符一一对应。#include <stdio.h>
#include <string.h>
int main() {
char result[100]; // 存储结果的字符数组
int id = 1001;
float score = 98.5;
// 用sprintf将id和score格式化为字符串
sprintf(result, "学生学号:%d,期末成绩:%.1f", id, score);
// 打印拼接后的字符串
printf("%s\n", result);
return 0;
}结果

我们可以发现这些函数都十分像,这里我用几个图来简单表示区别

函数组 | 函数名 | 功能 | 适用场景 |
|---|---|---|---|
输入函数组 | scanf | 格式化标准输入读取 | 从键盘(标准输入)读取 |
fscanf | 格式化文件输入读取 | 从文件输入流读取 | |
sscanf | 格式化字符串输入读取 | 从字符串中读取 | |
输出函数组 | printf | 格式化标准输出打印 | 输出到屏幕(标准输出) |
fprintf | 格式化文件输出打印 | 输出到文件输出流 | |
sprintf | 格式化字符串输出打印 | 输出到字符串中 |
由于字数原因,博主把文件操作分成两篇进行发布,下一篇我们将会讲到文件的随机读写和结束判定及缓冲区,本篇到这里就结束啦,本篇的函数不仅用在文件操作中,有的还可以对字符串的读取及转换等有帮助,相信大家都有所收获,有什么问题欢迎大家来评论区进行讨论呀,感谢大家的支持啦ヽ(≧∀≦)ノ!我们下一篇见!