核心问题:数据持久化存储
.c)
.obj)
.exe)
示例:C:\Users\Desktop\example.txt
存储方式 | 特点 | 文件大小 | 可读性 |
|---|---|---|---|
二进制文件 | 直接以内存中的二进制形式存储 | 较小 | 不可直接阅读 |
文本文件 | 以ASCII码形式存储 | 较大 | 可直接阅读 |
10000 的存储:
00100111 00010000(2字节)
'1''0''0''0''0'(5字节)

注意:用文本编辑器打开二进制文件会显示乱码,因为文本编辑器会以ASCII的形式翻译二进制序列
流是C语言中一个重要的抽象概念,它就像一条"数据的河流",数据在这条河流中流动。这个设计让程序员能够用统一的方式来处理各种不同的输入输出设备。
计算机需要与各种外部设备交互:
每种设备都有独特的操作方式,如果没有流的概念,程序员需要为每种设备编写特定代码,既繁琐又容易出错。
流的价值:提供统一抽象层,把所有输入输出设备都看作是"数据的河流",程序员只需要学会与"河流"打交道,而不需要关心具体设备细节。
C程序启动时自动打开三个标准流:
stdin - 标准输入流(通常为键盘)
stdout - 标准输出流(通常为显示器)
stderr - 标准错误流(通常为显示器)
在C语言中,我们通过**FILE***类型的文件指针来维护流的各种操作。这是缓冲文件系统中的核心概念。
FILE结构体变量,通常包含:
不同编译器对
FILE的具体定义略有差异
// VS2013中的FILE结构示例
struct _iobuf {
char *_ptr; // 缓冲区当前位置
int _cnt; // 剩余字符数
char *_base; // 缓冲区基地址
int _flag; // 文件状态标志
int _file; // 文件描述符
int _charbuf; // 字符缓冲区
int _bufsiz; // 缓冲区大小
char *_tmpfname; // 临时文件名
};
typedef struct _iobuf FILE;重要:作为使用者,我们不需要关心
FILE结构体的具体成员,系统会自动管理这些细节。
FILE* pf; // 创建一个文件指针变量
文件指针贯穿整个操作过程,是连接程序与文件的桥梁。
// 打开文件
FILE *fopen(const char *filename, const char *mode);
// 关闭文件
int fclose(FILE *stream);参数说明
模式 | 含义 | 文件要求 |
|---|---|---|
"r" | 只读 | 文件必须存在 |
"w" | 只写 | 创建新文件/清空已存在内容再写 |
"a" | 追加 | 创建新文件/在已有内容末尾添加 |
"r+" | 读写 | 文件必须存在 |
"w+" | 读写 | 创建新文件/清空已存在文件 |
"a+" | 读写 | 创建新文件/在已有文件末尾添加 |
二进制模式:在以上模式后加
b,如"rb"、"wb"
#include <stdio.h>
int main()
{
FILE *pFile;
// 打开文件 - 多种路径写法
pFile = fopen("myfile.txt", "w"); // 当前目录
pFile = fopen("C:/Users/Desktop/myfile.txt", "w"); // 绝对路径
pFile = fopen("./../myfile.txt", "w"); // 相对路径
// 检查文件是否打开成功
if (pFile == NULL)
{
perror("fopen"); // 输出错误信息
return -1;
}
// 文件操作...
// 关闭文件
fclose(pFile);
pFile = NULL; // 防止野指针
return 0;
}功能 | 函数名 | 适用流 |
|---|---|---|
字符输入 | fgetc | 所有输入流 |
字符输出 | fputc | 所有输出流 |
文本行输入 | fgets | 所有输入流 |
文本行输出 | fputs | 所有输出流 |
格式化输入 | fscanf | 所有输入流 |
格式化输出 | fprintf | 所有输出流 |
二进制输入 | fread | 文件 |
二进制输出 | fwrite | 文件 |
int fputc();
// 写字符到文件
for(int ch = 'a'; ch <= 'z'; ch++)
{
fputc(ch, pFile);
}
int fgetc();
// 从文件读取字符
int ch = 0;
while((ch = fgetc(pFile)) != EOF)
{
printf("%c ", ch);
}
// 标准流使用
int ch = fgetc(stdin); // 从键盘读取
fputc(ch, stdout); // 输出到屏幕参数说明
注意:这里
fputc和fgetc返回的是整型,需要用整型接收,因为其读取完后返回的是EOF,而EOF本质上是 -1
fputs();
// 写字符串到文件
fputs("Who you are?\n", pFile);
fputs("I am 失败的mian.\n", pFile);
fgets();
// 从文件读取字符串
char text[100] = {0};
fgets(text, 20, pFile); // 20中包括了\0,实际只读取了19个
// 当读取到\n时就会直接结束读取
// 标准流使用
char text[100] = {0};
fgets(text, 20, stdin); // 从键盘读取
fputs(text, stdout); // 输出到屏幕参数说明
返回NULL
printf(const char* format, …); // … 为可变参数列表
fprintf(FILE* stream, const char* format, …);
//
char name[] = "Zhangsan";
int age = 20;
float score = 95.5f;
fprintf(pFile, "name:%s\nage:%d\nscore:%.1f\n", name, age, score);
// 标准流使用
fprintf(stdout, "name:%s\nage:%d\nscore:%.1f\n", name, age, score);参数说明
scanf(const char* format, …);
fscanf(FILE* stream, const char* format, …);
//
char name[] = {0};
int age = 0;
float score = 0f;
fscanf(pFile, "%s %d %f", &name, &age, &sorce);
// 标准流使用
fscanf(stdin, "%s %d %f", &name, &age, &sorce);参数说明
// 把ptr中大小为size的count个数据写到stream中去
fwrite(const void* ptr, size_t size, size_t count, FILE* stream);
typedef struct
{
char name[];
int age;
float score;
}stu;
stu text = {"Zhangsan", 20, 95.5f};
fwrite(&text, sizeof(stu), 1, pFile);以
rb模式打开文件
// 从stream读取count个大小为size的数据到ptr中去
fread(const void* ptr, size_t size, size_t count, FILE* stream);
typedef struct
{
char name[];
int age;
float score;
}stu;
stu text = {0};
fread(&text, sizeof(stu), 1, pFile);以
wb的格式打开文件
返回实际读取的个数
对比一组函数
scanf/printf — 针对标准输入/输出流的格式化输入/输出函数
fscanf/fprintf — 针对所有输入/输出流的格式化输入/输出函数
sscnaf/sprintf —
sprintf — 其实是将格式化的数据写到字符串中
可以理解为:将格式化的数据转换成字符串
char text[100] = {0};
char name[] = "Zhangsan";
int age = 20;
float score = 95.5f;
sprintf(text, "name:%s age:%d score:%.1f", name, age, score);sscanf — 其实是从字符串中提取格式化的数据
可以理解为:将字符串转换成格式化的数据
char text[100] = "Zhangsan 20 95.5";
char name[] = {0};
int age = 0;
float score = 0f;
sscnaf(text, "%s %d %f", &name, &age, &score);同一次文件打开,对这个文件的每次操作光标位置不会自动重置,需要我们手动控制光标
fseek
根据文件指针的位置和偏移量来定位(不好理解,改成其它说法)文件指针(文件内容的光标)
// 从origin开始偏移offset个字符
int fseek(FILE* stream, long offset, int origin);参数说明:
offaet:偏移量
正负决定了偏移方向
origin:起始位置
SEEK_SET:文件的开头
SEEK_CUR:文件指针当前位置
SEEK_END:文件的末尾
ftell
返回文件指针(就是光标吗)相对于文件起始位置的偏移量
long ftell(FILE* stream);rewind
将文件指针重新定位到文件起始位置
long rewind(FILE* stream);7.1 被错误使⽤的 feof 牢记:在文件读取过程中,不能用feof函数的返回值直接来判断文件的是否结束。 feof 的作用是:当⽂件读取结束的时候,判断是读取结束的原因是否是:遇到文件尾结束。
• fgetc 判断是否为 EOF
• fgets 判断返回值是否为 NULL
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int c; // 注意:int,⾮char,要求处理EOF
FILE* fp = fopen("test.txt", "r");
if(!fp)
{
perror("File opening failed");
return EXIT_FAILURE;
}
// fgetc 当读取失败的时候或者遇到⽂件结束的时候,都会返回EOF
while ((c = fgetc(fp)) != EOF) // 标准C I/O读取⽂件循环
{
putchar(c);
}
// 判断是什么原因结束的
if (ferror(fp))
puts("I/O error when reading");
else if (feof(fp))
puts("End of file reached successfully");
fclose(fp);
pf = NULL;
}• fread判断返回值是否⼩于实际要读的个数。
#include <stdio.h>
enum { SIZE = 5 };
int main(void)
{
double a[SIZE] = {1.,2.,3.,4.,5.};
FILE *fp = fopen("test.bin", "wb"); // 必须⽤⼆进制模式
fwrite(a, sizeof *a, SIZE, fp); // 写double 的数组
fclose(fp);
double b[SIZE];
fp = fopen("test.bin","rb");
size_t ret_code = fread(b, sizeof *b, SIZE, fp); // 读 double 的数组
if(ret_code == SIZE) {
puts("Array read successfully, contents: ");
for(int n = 0; n < SIZE; ++n)
printf("%f ", b[n]);
putchar('\n');
}
else { // error handling
if (feof(fp))
printf("Error reading test.bin: unexpected end of file\n");
else (ferror(fp))
perror("Error reading test.bin");
}
fclose(fp);
pf = NULL;
}在读取文件的过程中,有可能文件读取结束
结束的原因:
1.遇到文件末尾
2.遇到错误了
EOF – end of file 文件结束标志
feof – 函数是用来判断文件是否结束的
但其实并不是
ANSIC标准采⽤“缓冲⽂件系统”处理的数据⽂件的,所谓缓冲文件系统是指系统自动地在内存中为 程序中每一个正在使⽤的⽂件开辟⼀块“文件缓冲区”。从内存向磁盘输出数据会先送到内存中的缓 冲区,装满缓冲区后才⼀起送到磁盘上。如果从磁盘向计算机读⼊数据,则从磁盘文件中读取数据输 ⼊到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓 冲区的大小根据C编译系统决定的。
// VS2022 WIN11 环境测试
#include <stdio.h>
#include <windows.h>
int main()
{
FILE*pf = fopen("test.txt", "w");
fputs("abcdef", pf); // 先将代码放在输出缓冲区
printf("睡眠10秒已经写数据了,打开test.txt⽂件,发现⽂件没有内容\n");
Sleep(10000);
printf("刷新缓冲区\n");
fflush(pf); // 刷新缓冲区时,才将输出缓冲区的数据写到⽂件(磁盘)
// 注:fflush 在⾼版本的VS上不能使⽤了
printf("再睡眠10秒此时,再次打开test.txt⽂件,⽂件有内容了\n");
Sleep(10000);
fclose(pf); // 注:fclose在关闭⽂件的时候,也会刷新缓冲区
pf = NULL;
return 0;
}避免单个程序频繁沟通系统,而占据大量系统内存资源 就像问问题一样,如果你一有问题就问老师,就会导致老师只能一直辅导你,而没有余力辅导别人了,缓冲区就是让你屯够了一定数量的问题再去问老师

这⾥可以得出⼀个结论: 因为有缓冲区的存在,C语言在操作文件的时候,需要做刷新缓冲区或者在⽂件操作结束的时候关闭文件。如果不做,可能导致读写⽂件的问题。
. 表示当前目录
.. 表示上级目录
注意事项:路径中尽量避免使用中文
NULL指针表示打开失败
perror函数输出具体错误信息
fclose不会自动将指针置为NULL
为什么使用文件?
如果没有文件,我们写的程序的数据是存储在电脑的内存中,如果程序退出,内存回收,数据就丢失了,等再次运行程序,是看不到上次程序的数据的,如果要将数据进行持久化的保存,我们可以使用文件
什么是文件?
存储于硬盘(磁盘)上的文件是文件,由于在磁盘上所以其可以实现持久化的保存
而内存只能临时存储
在程序设计中,我们通常谈的文件有两种:程序文件、数据文件(从文件功能角度来分类)
程序文件
源文件(.c)、目标文件(.obj)、可执行文件(.exe)
数据文件
文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行是需要从中读取数据的文件,或者输出内容的文件(本章讨论的是数据文件)
在以前各章所处理数据的输入输出都是以终端为
上,当需要的时候再从磁盘上把数据读取到内存中使用,这里处理的就是磁盘上的文件
文件名
一个文件要有一个唯一的标识,以便用户识别和引用
文件名包含三部分:文件路径 + 文件主干名 + 文件后缀(文件的身份证)
为了方便起见,文件标识常被称为文件名
(输入输出重定向)
二进制文件和文本文件?(从内容角度分类)
根据数据的组织形式,数据文件被称为文本文件或者二进制文件
一个数据在文件中是如何存储的呢?
数据在内存中以二进制的形式存储,如果不加转换的输出到外存的文件中,就是二进制文件
如果要求在外存上以ASCII码的形式存储,则需要在存储前转换。以ASCII字符的形式存储的文件就是文本文件一律以ASCII形式存储,数值型数据即可以用ASCII形式存储,也可以用二进制形式存储
ASCII形式存储数值很直观,但同样的数据如果以ASCII形式存储其占用的空间通常比二进制形式存储要大

(这也就解释为什么用文本编辑器打开二进制序列文件时显示乱码了)
文件的打开和关闭?
流和标准流
流
我们程序的数据需要输出到各种外部设备,也需要从外部设备获取数据,不同的外部设备的输入输出操作各不相同,为了方便程序员对各种设备进行操作,我们抽象出了流的概念,我们可以把流想像成流淌着字符的河
C程序针对文件、画面、键盘等数据的输入输出操作都是通过流操作的
一般情况下,我们要想向流里写数据,或者从流中读取数据,都是要打开流,然后操作,最后关闭流
标准流
为啥我们从键盘输入数据,向屏幕打上输出数据,没有打开流呢?
那是因为C语言程序在启动时,就默认打开了三个流
stdin - 标准输入流,在大多数的环境中从键盘输入,scanf函数就是重标准输入流中读取数据
stdout - 标准输出流,大多数的环境中输出到显示器界面,printf函数就是将数据输出到标准输出流中
stder - 标准错误流,大多数环境中输出到显示其界面
这是默认打开了这三个流,我们使用scanf、printf等函数就可以直接进行输入输出操作的
stdin、stdout、stderr三个流的类型是:FILE*,通常称为文件指针类型
文件指针
缓冲文件系统中,关键的概念是"文件类型指针",简称"文件指针",C语言中就是通过FILE*的文件指针来维护流的各种操作的
每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件名,文件状态,及文件当前的位置等)。这些信息是保存在一个结构体变量中的,该结构体类型是由系统声明的,取名FILE。
例如:VS2013编译环境提供的stdio.h头文件中有以下的文件类型申明()
// VS2013中的FILE结构示例
struct _iobuf
{
char *_ptr; // 缓冲区当前位置
int _cnt; // 剩余字符数
char *_base; // 缓冲区基地址
int _flag; // 文件状态标志
int _file; // 文件描述符
int _charbuf; // 字符缓冲区
int _bufsiz; // 缓冲区大小
char *_tmpfname; // 临时文件名
};
typedef struct _iobuf FILE;不同的C编译器的FILE的类型包含不完全相同,但是大同小异
每当打开一个文件的时候,系统会根据文件的情况自动创建一个FILE结构的变量,并填充其中的信息,使用者不必关系细节
一般都是通过一个FILE的指针来维护这个FILE结构的变量,这样使用起来更加方便
创建一个FILE*的指针变量
FILE* pf; // ⽂件指针变量定义pf是一个指向FILE类型数据的指针变量。可以使用pf指向某个文件信息区(是一个结构体变量)。通过该文件信息区中的信息就能够访问该文件。也就是说,通过文件指针变量能够间接找到与他关联的文件

文件在读写之前应该先打开文件流,在使用结束后应该关闭文件流
文件的操作:
1.打开文件流
2.操作文件流
3.关闭文件流
ANSI C
fopen(const char* filename, const char* mode); 文件名,打开方式
// 打开⽂件
FILE * fopen ( const char * filename, const char * mode );
// 关闭⽂件
int fclose ( FILE * stream )mode表示文件的打开模式,下面都是问的打开模式

示例
/* fopen fclose example */
#include <stdio.h>
int main ()
{
FILE * pFile;
// 打开⽂件
// 如果打开成功,返回的是有效指针
// 如果打开失败,返回的是空指针
// 如果打开的文件不存在,则会自动创建一个文件,否则,会先清空该文件内容再执行操作,不想清空就以追加模式打开
// 默认打开工程目录下的文件,其它目录下的文件需要带上路径(路径不要带中文)
pFile = fopen ("myfile.txt", "w"); // 以只读模式打开文件myfile.txt
// 用绝对路径打开
pFile = fopen ("C:/Users/Desktop/myfile.txt", "w");
// 用相对路径打开
// . 表示所在路径
// .. 表示上级路径
pFile = fopen ("./../myfile.txt", "w");
//⽂件操作
if (pFile == NULL) // 打开失败执行报错操作
{
perror("fopen");
return -1;
}
else
{
// 写文件(字符)
fputc('a', pFile);
for(int ch = 'a'; ch <= 'z'; ch++)
{
fputc(ch, pFile);
}
// 读文件(字符)
fgetc(pFile);
int ch = 0;
while((ch = fgetc(pFile)) != EOF)
{
printf("%c ", ch);
}
// 用于其它流
stdin --- 输入流
读的形式打开文件 -- 文件的输入流
stdout --- 输出流
写的形式打开文件 -- 文件的输出流
// 例如用于标准输入输出流读取
int ch = fgetc(stdin); // 从键盘(标准输入流)上读取
fputc(ch, stdout); // 将字符输出(写)到屏幕(标准输出流)
// 关闭⽂件
fclose (pFile);
pFile = NULL; // fclose 不会自动把指针置为空,所以我们还需要主动置空指针,
//否则,pFile就成了一个野指针
}
return 0;
}文件的顺序读写
顺序读写函数介绍
文本信息函数
二进制信息函数
上面说的适用于所有输入流一般指适用于标准输入流和其他输入流(如文件输入流);所有输出流一般指适用于标准输出流和其他输出流(如文件输出流)
对比一组函数
scanf/fscanf/sscanf
printf/fprintf/sprintf
文件的随机读写
fseek
ftell
rewind
文件读取结束的判定
文件缓冲区