C语言 --- 文件操作
一、什么是文件
文件是存储在磁盘/硬盘上的一种信息集合。
1.1文件分类
文件主要分为两大类:
(1)可执行文件
Windows系统下以.exe为后缀。
(2)数据文件
数据文件也分为文本文件和二进制文件。
本节介绍的文件操作都是对于数据文件而言的。
二、为什么要使用文件
就拿我们编写程序而言,截止目前我们所编写的程序都是内存级别的,当中的数据在程序执行结束后并不会保留下来,所以为了长久性/持久性的保存数据,就需要使用文件。
三、相关概念
3.1流
流是一个抽象概念,当我们要从外设(键盘,磁盘,网络等等)读数据,或者对外设写数据时,不同的外设进行读写操作时方式方法都不同,为了方便操作,同时减少记忆成本,就引入了这个流,此时读写操作就从流中读数据,向流写入数据。如下图所示:

引入流之后我们只关心如何对流进行操作,不用关心流对每一个外设的操作是如何的。
3.2标准流
有一个问题,为何我们也在使用scanf,printf从键盘获取数据,向显示器打印数据,为什么从来没有流这一个概念?
其实我们在启动C语言程序后会默认启动三个流,叫标准流。
stdin:从键盘获取数据的标准输入流。
stdout:向显示器/屏幕输出数据的标准输出流。
stderr:输出在显示器/屏幕的错误信息或者诊断信息的通道。
stdin,stdout,stderr的类型都是FILE*,也就是文件指针。
3.3文件指针
每当一个文件被打开时,会在内存中创建一个叫文件信息区(当中存放文件的名字,文件的大小等的属性)的一个结构体变量,与其对应文件相关联,并将次结构体变量typedef重命名为FILE*类型。如下图所示:
也就是说可以通过FILE*的文件指针来找到对应的文件。
四、文件操作相关函数
文件操作分为三步:打开文件,读/写文件,关闭文件。
4.1文件打开函数fopen()
FILE* fopen(const char* filename, const char* mode);
参数:
filename:文件所处路径。可以是从根目录开始的绝对路径也可以是相对路径,若只写了文件名称,则只会在当前.c文件所处目录下去寻找该文件;若使用''为路径分隔符,需要改为"\",因为单一个''会和后面的一个字符组成转义字符,而"\"本身转义就是'',使用'/'没有影响。
mode:文件打开方式。
| 打开方式 | 含义 | 文件不存在时 | 备注 |
|---|---|---|---|
| "r" | 只读 | 出错返回NULL | 最常用的只读模式 |
| "w" | 只写 | 创建新文件 | 谨慎使用,会覆盖原内容 |
| "a" | 追加 | 创建新文件 | 所有写入都追加到末尾 |
| "rb" | 二进制只读 | 出错返回NULL | 读取图片、音频、可执行文件等 |
| "wb" | 二进制只写 | 创建新文件 | 写入二进制数据 |
| "ab" | 二进制追加 | 创建新文件 | 追加二进制数据 |
| "r+" | 读写 | 出错返回NULL | 可读可写,不创建文件 |
| "w+" | 读写 | 创建新文件 | 读写模式,但会清空 |
| "a+" | 读写追加 | 创建新文件 | 读可以从任何位置,写只能在末尾 |
以上只列举了最常见且常用的几种mode,不代表全部。
返回值:
打开成功返回FILE的地址,失败返回NULL。
4.2关闭文件函数fclose()
int fclose(FILE* stream);
参数:
stream:关闭文件的文件指针,关闭后需要将这个文件指针设置为空指针,避免成为野指针。
4.3文件读写函数
4.3.1顺序读写函数
1)fputc()
int fputc(int char, FILE* stream);
功能:
向流中写入一个char字符。
返回值:
成功返回写入字符的ASCII值,失败返回EOF。
示例:
c
void FPUTC()
{
// 打开文件
// 向文件写入数据是"w"
FILE* pf = fopen("test.txt", "w");
if(pf == NULL)
{
perror("fopen:");
exit(1);
}
// 写文件
fputc('a', pf);
fputc('b', pf);
fputc('c', pf);
fputc('d', pf);
fputc('f', pf);
// 关闭文件
fclose(pf);
pf = NULL; // 避免成为野指针
}
运行结果:
我是Ubuntu系统下使用vim编写代码的,不是Windows系统。

2)fgetc()
int fgetc(FILE* stream);
功能:
从流中读取一个字符。
返回值:
成功返回读取字符的ASCII值,失败或者读取结束返回EOF。
示例:
c
void FGETC()
{
// 打开文件
// 从流中读取数据是"r"
FILE* pf = fopen("test.txt", "r");
if(pf == NULL)
{
perror("fopen:");
exit(1);
}
// 读文件
int ch = 0;
ch = fgetc(pf); // 读取pf中的一个字符
printf("%c ", ch); // 打印出来
ch = fgetc(pf); // 再读取
printf("%c ", ch);
ch = fgetc(pf); // 再读取
printf("%c ", ch);
// 关闭文件
fclose(pf);
pf = NULL; // 避免成为野指针
}
运行结果:

3)fputs()
int fputs(const char* str, FILE* stream);
功能:
向流中写入一行str字符串,会识别\n,使其当前文本换行。
返回值:
成功返回一个非负数,失败返回EOF。
示例:
c
void FPUTS()
{
// 打开文件
FILE* pf = fopen("test.txt", "w");
if(pf == NULL)
{
perror("fopen:");
exit(1);
}
// 写文件
// 不写\n会输出在一行上
fputs("hello world!!!\n", pf);
fputs("hello dyj!!!\n", pf);
// 关闭文件
fclose(pf);
pf = NULL;
}
运行结果:

4)fgets()
char* fgets(char* str, int size, FILE* stream);
功能:
从流中读取一行str字符串给到我们给定的str中,最大字符大小为为size(包含'\0')。
返回值:
成功返回str,失败或者读取结束返回NULL。
示例:
c
void FGETS()
{
// 打开文件
FILE* pf = fopen("test.txt", "r");
if(pf == NULL)
{
perror("fopen:");
exit(1);
}
// 读文件
char arr[100] = { 0 };
fgets(arr, 100, pf);
printf("%s ", arr);
fgets(arr, 100, pf);
printf("%s ", arr);
// 关闭文件
fclose(pf);
pf = NULL;
}
运行结果:

5)fprintf()
int fprintf(FILE* stream, const char* format, ...);
对比printf,就只多了一个FILE* stream。
功能:
向流中写入格式化的数据(也就是各种类型的数据),若stream为标准输出流stdout,则此函数和printf没有区别。
示例:
将结构体s的数据写入到test.txt文件中。
c
struct S
{
char arr[20];
int a;
double pai;
};
void FPRINTF()
{
// 打开文件
FILE* pf = fopen("test.txt", "w");
if(pf == NULL)
{
perror("fopen:");
exit(1);
}
// 写文件
struct S s = { "hello!!", 100, 3.14 };
fprintf(pf, "%s %d %.2lf ", s.arr, s.a, s.pai);
// 关闭文件
fclose(pf);
pf = NULL;
}
运行结果:
当前结果没有换行。

6)fscanf()
int fscanf(FILE* stream, const char* format, ...);
对比scanf,也就只多了一个FILE* stream。
功能:
从流中获取格式化的数据(也就是各种类型的数据),若stream为标准输入流stdin,则此函数和scanf没有区别。
示例:
将test.txt文件中的数据写入到一个新的结构体变量s中。
c
struct S
{
char arr[20];
int a;
double pai;
};
void FSCANF()
{
// 打开文件
FILE* pf = fopen("test.txt", "r");
if(pf == NULL)
{
perror("fopen:");
exit(1);
}
// 读文件
struct S s = { 0 };
fscanf(pf, "%s %d %lf", s.arr, &(s.a), &(s.pai));
// stdout,则功能和printf没有区别
fprintf(stdout, "s的arr = %s s的a = %d s的pai = %.2lf", s.arr, s.a, s.pai);
// 关闭文件
fclose(pf);
pf = NULL;
}
运行结果:

7)fwrite()
size_t fwrite(const void* ptr, size_t size, size_t nmemd, FILE* stream);
功能:
将ptrh指向的任意类型(void*)的数据,以单个元素字节大小size,元素个数nmemd个,写入到指定文件流中。
参数:
ptr:void*类型,意味着可以指向任意类型的数据。
size:指向元素中单个元素的字节大小。
nmemd:总共的元素个数。
stream:指定文件流。
返回值:
实际写入的元素个数。
示例:
c
void FWRITE()
{
// 打开文件
FILE* pf = fopen("data.txt", "wb");
if(pf == NULL)
{
perror("fopen:");
exit(1);
}
// 写数据
int arr[] = { 1,2,3,4,5 };
// 写的参数形式不止我写的这一种
fwrite(arr, sizeof(arr[0]), 5, pf);
// 关闭文件
fclose(pf);
pf = NULL;
}
运行结果:
在Ubuntu系统中以十六进制形式查看一个二进制文件内容的命令为:xxd 文件名,并且显示形式受大小端影响,是倒着显示的。

8)fread()
size_t fread(const void* ptr, size_t size, size_t nmemd, FILE* stream);
功能:
从指定文件流中,将二进制数据写入到ptr指向的空间中,也是以单个元素字节大小size,元素个数nmemd个写入。
参数:
ptr:void*类型,意味着可以指向任意类型的数据。
size:指向元素中单个元素的字节大小。
nmemd:总共的元素个数。
stream:指定文件流。
示例:
c
void FREAD()
{
// 打开文件
FILE* pf = fopen("data.txt", "rb");
if(pf == NULL)
{
perror("fopen:");
exit(1);
}
// 写数据
int arr[10] = { 0 };
fread(arr, sizeof(int), 5, pf);
// 打印arr数组
int i = 0;
for(; i < 5; i++)
{
fprintf(stdout, "%d ", arr[i]);
}
// 关闭文件
fclose(pf);
pf = NULL;
}
运行结果:

4.3.2随机读写函数
1)fseek()
int fseek(FILE* stream, long offset, int whence);
功能:
从whence规定的文件位置开始定位至偏移量为offset的元素。
参数:
stream:指定文件流。
offset:偏移量。
whence:从文件哪个位置开始,有三个固定取值,SEEK_SET(文件开始位置)、SEEK_CUR(文件当前光标位置)、SEEK_END(文件末尾位置)。
示例:
c
void FSEEK()
{
// 打开文件
FILE* pf = fopen("test.txt", "r");
if(pf == NULL)
{
perror("fopen:");
exit(1);
}
// 先读文件中第一个字符
int ch = fgetc(pf);
fputc(ch, stdout); // 打印a
// fseek(pf, 2, SEEK_SET); // 因为是从文件开始(SEEK_SET)定位两个偏移量至c
// fseek(pf, 2, SEEK_CUR); // 因为是从文件当前光标位置(SEEK_CUR)b定位两个偏移量至d
// fseek(pf, -2, SEEK_END); // 因为是从文件末尾(SEEK_END)向前定位两个偏移量至d
// 再读一个
ch = fgetc(pf);
fputc(ch, stdout); // (SEEK_SET)打印c、(SEEK_CUR)打印d、(SEEK_END)打印d
// 关闭文件
fclose(pf);
pf = NULL;
}
2)ftell()
long ftell(FILE* stream);
功能:获取当前文件光标位置。
3)frewind()
void rewind(FILE* stream);
功能:重新将文件光标位置定位至文件开始。
五、文件缓冲区
ANSI C 标准采用"缓冲文件系统" 处理数据⽂件的,所谓缓冲文件系统是指系统自动地在内存中为程序中每⼀个正在使用的文件开辟一块"文件缓冲区"。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才⼀起送到磁盘上。如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的大小根据C编译系统决定的。
文件关闭函数fclose(),执行时会自动刷新缓冲区;缓冲区强制刷新函数fflush(FILE* stream),针对所有的流都可以刷新。
六、结语
C语言的文件操作函数不止我再博客中讲的这些,如有兴趣可以去了解其他的文件操作函数,谢谢大家看到此处,这是对我这一篇拙作的最大的支持!!!