
🔥个人主页:@草莓熊Lotso的个人主页 🎬作者简介:C++研发方向学习者 📖个人专栏:《C语言》 ⭐️人生格言:生活是默默的坚持,毅力是永久的享受。
--如果没有文件,我们写的程序的数据是存储在电脑的内存中,如果程序退出,内存回收,数据就丢失了,等再次运行程序,是看不到上次程序的数据的,如果要将数据进行持久化的保存,我们可以使用文件。
--磁盘(硬盘)上的文件是文件。但是在程序设计中,我们⼀般谈的文件有两种:程序文件、数据文件(从文件功能的角度来分类的)。
程序文件:
数据文件:
--本篇文章主要介绍的是数据文件,在之前的学习中我们所处理数据的输入输出都是以终端为对象的 即从终端的键盘输入数据,运行结果显示到屏幕上。
但其实有时候我们会把信息输出到磁盘上,当需要的时候再从磁盘上把数据读取到内存中使用,这里处理的就是磁盘上的文件

--一个文件要有唯一个的文件标识,以便我们识别和使用。
文件名包含3个部分:文件路径+文件名主干+文件后缀
例如:D:\code\test.txt

根据数据的组织形式,数据文件被分为文本文件和二进制文件。
一个数据在文件中如何存储呢?

代码演示:
#include <stdio.h>
int main()
{
int a = 10000;
FILE* pf = fopen("data.txt", "wb");
if (pf == NULL)
{
perror("fopen");
return 1;
}
fwrite(&a, 4, 1, pf);//二进制的形式写到文件中
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}我们默认打开的会是文本文件,观察不了二进制形式,所以我们需要在VS上打开二进制文件 ,具体操作如下:
我们把data.txt的打开方式设置为二进制编辑器,就可以观察二进制文件了。



我们程序的数据需要输出到各种外部设备,也需要从外部设备获取数据,不同的外部设备的输入输出操作各不相同,为了方便程序员对各种设备进行方便的操作,我们抽象出了流的概念,我们可以把流想象成流淌着字符的河。
C程序针对文件、画面、键盘等的数据输入输出操作都是通过流操作的。⼀般情况下,我们要想向流里写数据,或者从流中读取数据,都是要打开流,然后操作。

那为什么我们从键盘输入数据,向屏幕上输出数据,并没有打开流呢?
这是因为C语言程序在启动的时候,默认打开了3个流:
这就是是默认打开的三个流,所以我们使用scanf、printf等函数就可以直接进行输入输出操作。stdin、stdout、stderr 三个流的类型是: FILE * ,通常称为文件指针。 C语言中,就是通过 FILE* 的文件指针来维护流的各种操作的。
--缓冲文件系统中,关键的概念是“文件类型指针”,简称“文件指针”。
每个被使用的文件都在内存中开辟了⼀个相应的文件信息区,用来存放文件的相关信息(如文件的名字,文件状态及文件当前的位置等)。这些信息是保存在一个结构体变量中的。该结构体类型是由系统声明的,取名为: FILE.
例如:VS2013 编译环境提供的 stdio.h 头文件中有以下的文件类型申明
struct _ iobuf { char *_ptr; int _cnt; char *_base; int _flag; int _file; int _charbuf; int _bufsiz; char *_tmpfname; }; typedef struct _ iobuf FILE ;
不同的编译器FILE类型包含的内容不完全相同,每次打开文件,系统都会自动创建一个FILE结构的变量,并填充其中信息,我们不需要关心这个
我们一般都是通过一个FILE指针来维护这个结构变量,这样使用起来更加方便。
创建形式如下:
FILE* pf; //文 件指针变量
定义pf是⼀个指向FILE类型数据的指针变量。可以使pf指向某个文件的文件信息区(是以个结构体变量)。通过该文件信息区中的信息就能够访问该文件。也就是说,通过文件指针变量能够间接找到与它关联的文件。

--文件在读写之前应该先打开文件,在使用结束之后应该关闭文件。
在编写程序的时候,在打开文件的同时,都会返回⼀个FILE*的指针变量指向该⽂件,也相当于建立了指针和文件的关系。
我们使用 fopen 函数来打开文件, fclose 来关闭文件。
//打开文件 1. FILE * fopen ( const char * filename , const char * mode );
功能:
fopen函数是用来打开参数filename所指定的文件的,同时将其和一个流进行关联,后续对流的操作是通关fopen函数返回的指针来维护的。具体对流的操作是通过参数mode指定的
参数:
返回值:
代码演示:这里演示5种不同情况
//1.data.txt在当前工程的目录下
int main()
{
FILE* pf = fopen("data.txt", "r");
//这里的路径是相对路径,表示在当前的工程目录下的data.txt
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读取文件
//关闭文件
return 0;
}
//2. . --表示当前路径, .. --表示上一级路径
//所以如果data.txt在上一级路径下的时候应该这样写
int main()
{
FILE* pf = fopen("./../data.txt", "r");
//这里的路径是相对路径,表示在当前的工程目录下的data.txt
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读取文件
//关闭文件
return 0;
}
//3.如果data.txt在上二级路径下的时候应该这样写
int main()
{
FILE* pf = fopen("./../../data.txt", "r");
//这里的路径是相对路径,表示在当前的工程目录下的data.txt
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读取文件
//关闭文件
return 0;
}
//4.如果data.txt在上二级路径下的文件夹text中的text2文件夹里的时候应该这样写
int main()
{
FILE* pf = fopen("./../../text/text2/data.txt", "r");
//这里的路径是相对路径,表示在当前的工程目录下的data.txt
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读取文件
//关闭文件
return 0;
}
//5.如果data.txt在桌面上,就得用绝对路径了
int main()
{
FILE* pf = fopen("C:/Users/LOTSO/Desktop/data.txt", "r");
//这里的路径是绝对路径
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读取文件
//关闭文件
return 0;
}mode--文件操作方式:
文件使用方式 | 含义 | 如果指定文件不存在 |
|---|---|---|
"r"(只读) | 为了输入数据,打开一个已经存在的文本文件 | 出错 |
"w"(只写) | 为了输出数据,打开⼀个文本文件 | 建立⼀个新的文件 |
“a”(追加) | 向文本文件尾添加数据 | 建立⼀个新的文件 |
“rb”(只读) | 为了输入数据,打开⼀个二进制文件 | 出错 |
"wb"(只写) | 为了输出数据,打开⼀个二进制文件 | 建立⼀个新的文件 |
“ab”(追加) | 向⼀个二进制文件尾添加数据 | 建立⼀个新的文件 |
“r+”(读写) | 为了读和写,打开⼀个文本文件 | 出错 |
“w+”(读写) | 为了读和写 建立⼀个新的文件 | 建立一个新的文件 |
“a+”(读写) | 打开⼀个文件,在文件尾进行读写 | 建立一个新的文件 |
“rb+”(读写) | 为了读和写打开⼀个二进制文件 | 出错 |
“wb+”(读 写) | 为了读和写,新建⼀个新的二进制文件 | 建立一个新的文件 |
“ab+”(读 写) | 打开⼀个二进制文件,在文件尾进行读和写 | 建立一个新的文件 |
----------------- | ------------------------------------------------------ | ------------------------ |
1. int fclose ( FILE * stream );
功能:
关闭参数 stream 关联的文件,并取消其关联关系。与该流关联的所有内部缓冲区均会解除关联并刷新:任何未写入的输出缓冲区内容将被写入,任何未读取的输入缓冲区内容将被丢弃。
参数:
返回值:成功关闭 stream 指向的流会返回0,否则会返回 EOF 。
代码演示:
#include<stdio.h>
int main()
{
FILE* pf = fopen("data.txt", "r");
//这里的路径是相对路径,表示在当前的工程目录下的data.txt
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读取文件
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}在进行文件写的时候,我们会涉及下面这些函数:
函数名 | 功能 | 适用于 |
|---|---|---|
fgetc | 从输入流中读取一个字符 | 所有输入流 |
fputc | 向输出流中写入一个字符 | 所有输出流 |
fgets | 从输入流中读取一个字符串 | 所有输入流 |
fputs | 向输出流中写入一个字符串 | 所有输出流 |
fscanf | 从输入流中读取带有格式的数据 | 所有输入流 |
fprintf | 向输出流中写入带有格式的数据 | 所以输出流 |
fread | 从输入流中读取一块数据 | 文件输入流 |
fwrite | 从输出流中写入一块数据 | 文件输出流 |
1. int fputc ( int character, FILE * stream );
功能:
将参数 character 指定的字符写入到 stream 指向的输出流中,通常用于向文件或标准输出流写入字符。在写入字符之后,还会调整指示器。字符会被写入流内部位置指示器当前指向的位置,随后该指示器自动向前移动⼀个位置。
参数:
返回值:
代码演示:
//fputc函数演示
#include<stdio.h>
int main()
{
FILE* ps = fopen("data.txt", "w");//w--写,用这个才可以写文件
if (ps == NULL)
{
perror("fopen");
return 1;
}
//写文件
fputc('a', ps);
fputc('b', ps);
fputc('c', ps);
//关闭文件
fclose(ps);
ps = NULL;
return 0;
}
//循环写入多个字符
int main()
{
FILE* ps = fopen("data.txt", "w");//w--写,用这个才可以写文件
if (ps == NULL)
{
perror("fopen");
return 1;
}
//写文件
for (int i = 'a';i <= 'z';i++)
{
fputc(i, ps);
}
//关闭文件
fclose(ps);
ps = NULL;
return 0;
}
//直接打印在屏幕上,标准输出流
#include<stdio.h>
int main()
{
fputc('a', stdout);
fputc('b', stdout);
fputc('c', stdout);
return 0;
}1. int fgetc ( FILE * stream );
功能:
从参数 stream 指向的流中读取一个字符。函数返回的是文件指示器当前指向的字符,读取这个字符之后,文件指示器自动前进道下⼀个字符。
参数:
返回值:
代码演示:
//fgetc函数演示,假设文件里是26个字母
#include<stdio.h>
int main()
{
FILE* ps = fopen("data.txt", "r");
if (ps == NULL)
{
perror("fopen");
return 1;
}
//读文件
for (int i = 0;i < 10;i++)
{
int c=fgetc(ps);
fputc(c, stdout);
}
//关闭文件
fclose(ps);
ps = NULL;
return 0;
}
////从键盘读取数据
#include<stdio.h>
int main()
{
//读数据
int c=fgetc(stdin);
fputc(c, stdout);
return 0;
}int feof ( FILE * stream ); // 检测 stream 指针指向的流是否遇到文件末尾 int ferror ( FILE * stream ); // 检测 stream 指针指向的流是否发生读 / 写错误
测试feof:
//测试feof,假设文件里是abcdef
#include<stdio.h>
int main()
{
FILE* ps = fopen("data.txt", "r");
if (ps == NULL)
{
perror("fopen");
return 1;
}
//读文件
int i = 0;
for (i = 0;i < 10;i++)
{
int c = fgetc(ps);
if (c == EOF)
{
if (feof(ps))
{
printf("遇到文件结尾了\n");//读到f就结尾了
}
else if (ferror(ps))
{
printf("读取发生错误\n");
}
}
else
{
fputc(c, stdout);
}
}
fclose(ps);
ps = NULL;
return 0;
}
测试ferror:
//测试ferror,
// 以写的形式打开文件后,后面再读文件,就会发生错误
#include<stdio.h>
int main()
{
FILE* ps = fopen("data.txt", "w");
if (ps == NULL)
{
perror("fopen");
return 1;
}
//读文件
int i = 0;
for (i = 0;i < 6;i++)
{
int c = fgetc(ps);
if (c == EOF)
{
if (feof(ps))
{
printf("遇到文件结尾了\n");
}
else if (ferror(ps))
{
printf("读取文件错误\n");
}
}
else
{
fputc(c, stdout);
}
}
fclose(ps);
ps = NULL;
return 0;
}
1. int fputs ( const char * str, FILE * stream );
功能:
将参数 str 指向的字符串写入到参数 stream 指定的流中(不包含结尾的空字符 \0 ),适用于文件流或标准输出(stdout)。
参数:
返回值:
代码演示:
//fputs函数演示
#include<stdio.h>
int main()
{
FILE* ps = fopen("data.txt", "w");
if (ps == NULL)
{
perror("fopen");
return 1;
}
//写文件
fputs("abc\n", ps);
fputs("def", ps);
//关闭文件
fclose(ps);
ps = NULL;
return 0;
}
和fputc函数一样,我们也可以直接打印在屏幕上,把ps改成stdout就可以,这里就不再演示了。
1. char * fgets ( char * str, int num, FILE * stream );
功能:
从 stream 指定输入流中读取字符串,至读取到换行符、文件末尾(EOF)或达到指定字符数
(包含结尾的空字符 \0 ),然后将读取到的字符串存储到str指向的空间中。
参数:
返回值:
代码演示:
// fgets函数演示,假设文件里还是上面fputs函数写的 abc\n def
#include<stdio.h>
int main()
{
FILE* fp = fopen("data.txt", "r");
if (fp == NULL)
{
perror("fopen");
return 1;
}
char arr1[] = "*************";
/*char* f = fgets(arr1, sizeof(arr1), fp);
fputs(f, stdout);*///全部打印出来abc
char*p=fgets(arr1,2, fp);//实际读取的是num-1个
fputs(p,stdout);//所以是a
//不再使用文件时,需要关闭文件
fclose(fp);
fp = NULL; //将指针置为NULL,避免成为野指针。
return 0;
}
我们再来看一个例子并且通过调试以及打印在屏幕上来观察:
//把abc\n def全读出来,调试着看并打印在屏幕上
#include <stdio.h>
int main()
{
FILE* fp = fopen("data.txt", "r");
if (fp == NULL)
{
perror("fopen\n");
return 1;
}
char arr1[] = "*************";
char*p=fgets(arr1, sizeof(arr1), fp);
fputs(p, stdout);
char arr2[] = "*************";
char* f = fgets(arr2, sizeof(arr1), fp);
fputs(f, stdout);
//不再使用文件时,需要关闭文件
fclose(fp);
fp = NULL; //将指针置为NULL,避免成为野指针。
return 0;
}

和fgetc函数一样,我们也可以直接从键盘上读取,把fp改成stdin就可以,这里也不再演示了。
1. int fprintf ( FILE * stream, const char * format, ... ); 2. //我们类比printf看看 3. int printf ( const char * format, ... );
功能:
fprintf 是将格式化数据写入指定文件流的函数。它与 printf 类似,但可以输出到任意文件(如磁盘文件、标准输出、标准错误等),而不仅限于控制台。
参数:
返回值:
代码演示:
// fprintf函数演示
#include <stdio.h>
struct stu
{
char name[30];
int age;
float score;
};
int main()
{
struct stu s = { "zhangsan",18,95.5f};
FILE* fp = fopen("data.txt", "w");
if (fp == NULL)
{
perror("fopen");
return 1;
}
//写文件
fprintf(fp, "%s %d %f", s.name, s.age, s.score);
//关闭文件
fclose(fp);
fp = NULL;
return 0;
}
当然这也可以直接打印在屏幕上,还是把fp换成stdout,这样效果等同于printf,这里就不演示了。
1. int fscanf ( FILE * stream, const char * format, ... ); 2. //类比scanf看看 3. int scanf ( const char * format, ... );
功能:
fscanf 是从指定文件流中读取格式化数据的函数。它类似于 scanf ,但可以指定输入源(如文件、标准输入等),而非仅限于控制台输⼊。适用于从文件解析结构化数据(如整数、浮点数、字符串等)。
参数:
返回值:
成功时,函数返回成功填充到参数列表中的项数。该值可能与预期项数⼀致,也可能因以下原因少
于预期(甚至为零):
如果在成功读取任何数据之前发生:
代码演示:
//fsanf函数演示
struct stu
{
char name[30];
int age;
float score;
};
int main()
{
struct stu s = { 0 };
FILE* fp = fopen("data.txt", "r");
if (fp == NULL)
{
perror("fopen");
return 1;
}
//读文件
fscanf(fp, "%s %d %f", s.name, &(s.age), &(s.score));
fprintf(stdout, "%s %d %f", s.name, s.age, s.score);
//打印数据在stdout
//关闭文件
fclose(fp);
fp = NULL;
return 0;
}
当然这也可以直接从键盘上读取,还是把fp换成stdin,这样效果等同于scanf,这里就不演示了。
--由于篇幅原因,后面还有几个函数,我们在下篇文章再一起来继续学习。
往期回顾: 【C语言动态内存管理】--动态内存分配的意义,malloc和free,calloc和realloc,常见的动态内存的错误,动态内存经典笔试题分析,柔性数组,总结C/C++中程序内存区域划分 【自定义类型-结构体】--结构体类型,结构体变量的创建和初始化,结构体内存对齐,结构体传参,结构体实现位段 【自定义类型-联合和枚举】--联合体类型,联合体大小的计算,枚举类型,枚举类型的使用
结语:本篇文章就到此结束了,继前面一篇文章后,在此篇文章中给大家分享了文件操作中的文件的意义和概念,二进制文件和文本文件,文件的打开和关闭,文件的顺序读写(部分)等知识点,下篇文章会接着从文件的顺序读写后面的几个函数开始。分析文件操作中的剩余知识点,如果文章对你有帮助的话,欢迎评论,点赞,收藏加关注,感谢大家的支持。