1. 为什么要使用文件
- 程序运行时,数据通常存储在内存中,当程序退出后,内存中的数据会被释放,无法长期保存。
- 文件的核心作用是实现数据的持久化存储,将数据保存在外部存储设备(如硬盘)中,即使程序关闭,数据也不会丢失,下次运行程序时可重新读取使用。
- 示例场景:
- 通讯录程序:若不使用文件,每次运行程序后添加的联系人会随程序退出而消失;使用文件可将联系人数据保存,下次打开程序时读取文件恢复数据。
- 游戏存档:游戏进度需通过文件保存,否则关闭游戏后进度全部丢失,无法继续上次游戏。
2. 什么是文件
- 文件是存储在外部存储设备上的数据流,按用途可分为程序文件和数据文件。
2.1 程序文件
- 定义:用于程序编译、链接和运行的文件,包含程序的代码和指令。
- 类型及示例:
- 源文件(.c):如
main.c
,存放C语言源代码。 - 目标文件(.obj/.o):编译后生成的二进制文件,如
main.obj
。 - 可执行文件(.exe):链接后生成的可运行文件,如
main.exe
。
- 源文件(.c):如
2.2 数据文件
- 定义:程序运行时读写的文件,用于存储程序处理的数据(如文本、数值等)。
- 操作方向:
- 写操作:程序(.c文件编译后的可执行文件)将数据输出到数据文件中。
- 读操作:程序从数据文件中读取数据并处理。
- 示例:
- 程序将用户输入的账号密码写入
user.txt
(写操作)。 - 程序从
score.dat
中读取学生成绩并计算平均分(读操作)。
- 程序将用户输入的账号密码写入
2.3 什么是文件名
- 文件名是唯一标识一个文件的字符串,用于在文件系统中定位文件,其组成格式为:
路径 + 文件名主干 + 后缀
。 - 各部分含义:
- 路径:表示文件在存储设备中的位置(如
D:\data\
)。 - 文件名主干:文件的核心名称(如
test
)。 - 后缀:标识文件类型(如
.txt
表示文本文件,.dat
表示数据文件)。
- 路径:表示文件在存储设备中的位置(如
- 示例:
C:\project\result.txt
:绝对路径,完整标识了文件位置。./logs/error.log
:相对路径(相对于当前程序运行目录)。- 路径中的转义字符:在C语言字符串中,
\
需用\\
表示(转义),如"D:\\data\\info.dat"
。
3. 文件的打开和关闭
3.1 文件指针
- 缓冲文件系统中,当打开一个文件时,系统会自动创建一个FILE类型的结构体变量(称为文件信息区),该结构体包含了与文件相关的所有信息:
- 文件的名称、存储路径;
- 文件的打开方式(读/写/追加);
- 文件当前的读写位置;
- 文件的状态(是否出错、是否已到末尾)等。
- 这个FILE结构体由系统定义(不同编译器的实现细节可能不同,但核心成员一致),使用者无需关心其内部结构,只需通过指向该结构体的指针来操作文件。
- 文件指针的定义:通过
FILE*
类型声明一个指针变量,用于指向上述FILE结构体。FILE* pf; // pf就是一个文件指针变量,未指向任何文件时为NULL
- 文件指针的作用:当通过fopen打开文件时,函数会返回一个指向该文件信息区(FILE 结构体)的指针,将其赋值给文件指针后,后续对文件的所有操作(读 / 写 / 关闭)都通过该指针完成。
- 示例:文件指针与文件的关联过程
// 1. 打开文件,fopen返回指向该文件信息区的指针
FILE* pf = fopen("test.txt", "r");
if (pf == NULL) { // 若打开失败,返回NULL
perror("fopen failed");
return 1;
}
// 2. 此时pf指向test.txt的文件信息区,通过pf操作文件
// 例如:读取文件内容、获取文件状态等
// 3. 操作完成后,通过pf关闭文件
fclose(pf);
pf = NULL; // 关闭后将指针置空,避免成为野指针
-
关键说明:
- 一个文件指针只能关联一个文件,若要操作多个文件,需定义多个文件指针:
FILE* pf1 = fopen("a.txt", "r"); // pf1关联a.txt FILE* pf2 = fopen("b.txt", "w"); // pf2关联b.txt
-
文件指针本质是通过指向文件信息区,间接获取文件的所有状态和位置信息,从而实现对物理文件的操作,它是程序与文件之间的 “桥梁”。
3.2 文件的打开和关闭
- 文件操作流程:打开文件→读写文件→关闭文件。打开文件后必须手动关闭,否则可能导致数据丢失或资源泄漏。
3.2.1 打开文件:fopen函数
-
函数原型:
FILE* fopen(const char* filename, const char* mode);
-
参数:
filename
:文件名(含路径和后缀,注意\\
转义)。mode
:打开方式(常用方式:r
读,w
写,a
追加)。
-
返回值:成功返回文件指针(指向文件信息区),失败返回
NULL
(需检查返回值)。 -
打开方式示例:
r
(只读):打开已存在的文件,用于读取;文件不存在则打开失败。w
(只写):创建新文件或清空已有文件,用于写入;若文件存在,原有内容会被删除。a
(追加):打开文件在末尾追加内容;文件不存在则创建,原有内容保留。
-
路径示例:
// 绝对路径:打开D盘data目录下的test.txt(注意\\转义) FILE* pf1 = fopen("D:\\data\\test.txt", "r"); // 相对路径:打开当前程序目录下的log.txt FILE* pf2 = fopen("log.txt", "w"); // 检查打开是否成功 if (pf1 == NULL || pf2 == NULL) { perror("fopen failed"); // 打印错误信息 return 1; }
3.2.2 关闭文件:fclose 函数
- 函数原型:int fclose(FILE* stream);
- 参数:stream为要关闭的文件指针。
- 返回值:成功返回 0,失败返回非 0。
示例:
// 关闭文件
if (fclose(pf1) != 0) {
perror("fclose failed");
}
if (fclose(pf2) != 0) {
perror("fclose failed");
}
pf1 = NULL; // 避免野指针
pf2 = NULL;
3.2.3 完整示例(写文件)
// 打开文件(w模式,若存在则清空内容)
FILE* pf = fopen("test.txt", "w");
if (pf == NULL) {
perror("fopen failed");
return 1;
}
// 写入数据
fputs("hello world", pf);
// 关闭文件
fclose(pf);
pf = NULL;
3.2.4 完整示例(追加文件)
// 打开文件(a模式,在末尾追加)
FILE* pf = fopen("test.txt", "a");
if (pf == NULL) {
perror("fopen failed");
return 1;
}
// 追加数据(原有内容不会被删除)
fputs("\nappend content", pf);
// 关闭文件
fclose(pf);
pf = NULL;
4. 文件的顺序读写
4.1 字符输入函数 fgetc
-
函数原型:
int fgetc(FILE* stream);
-
功能:从指定的流(
stream
)中读取一个字符(以int
类型返回,包含EOF)。 -
参数:
stream
为文件指针(如pf
)或标准输入流(stdin
,对应键盘输入)。 -
返回值:成功返回读取的字符(ASCII值),失败或读到文件末尾返回
EOF
(通常为-1)。 -
什么是流:流是数据输入/输出的抽象概念,C语言中所有输入/输出操作都通过流完成。常见流包括:
- 文件流:通过
fopen
打开的文件(如pf = fopen("test.txt", "r")
)。 - 标准流:程序启动时默认打开的3个流(
stdin
、stdout
、stderr
)。
- 文件流:通过
-
示例1:从文件读取字符
FILE* pf = fopen("test.txt", "r");
if (pf == NULL) {
perror("fopen failed");
return 1;
}
int ch;
// 循环读取文件,直到遇到EOF
while ((ch = fgetc(pf)) != EOF) {
printf("%c", ch); // 打印读取的字符(输出到stdout)
}
fclose(pf);
pf = NULL;
- 示例 2:从标准输入流(键盘)读取字符
int ch = fgetc(stdin); // 从键盘读取一个字符(如输入'a')
printf("你输入的字符是:%c\n", ch); // 输出'a'
4.2 字符输出函数 fputc
-
函数原型:
int fputc(int character, FILE* stream);
-
功能:将一个字符(
character
)写入指定的流(stream
)。 -
参数:
character
为要写入的字符(int
类型,实际取低8位);stream
为文件指针或标准输出流(stdout
,对应屏幕)。 -
返回值:成功返回写入的字符,失败返回
EOF
。 -
示例1:向文件写入字符
FILE* pf = fopen("test.txt", "w");
if (pf == NULL) {
perror("fopen failed");
return 1;
}
// 写入字符'A'、'B'、'C'
fputc('A', pf);
fputc('B', pf);
fputc('C', pf); // 文件内容为"ABC"
fclose(pf);
pf = NULL;
- 示例 2:向标准输出流(屏幕)输出字符
fputc('H', stdout);
fputc('i', stdout); // 屏幕输出"Hi"
4.3 文本行输出函数 fputs
-
函数原型:
int fputs(const char* str, FILE* stream);
-
功能:将字符串(
str
)写入指定的流(stream
),不自动添加换行符(\n
)。 -
参数:
str
为要写入的字符串(以\0
结尾);stream
为文件指针或标准输出流。 -
返回值:成功返回非负值,失败返回
EOF
。 -
示例1:向文件写入字符串(手动换行)
FILE* pf = fopen("test.txt", "w");
if (pf == NULL) {
perror("fopen failed");
return 1;
}
// 写入两行字符串(需手动添加\n)
fputs("Hello World", pf);
fputs("\nWelcome to C", pf); // 文件内容:Hello World\nWelcome to C
fclose(pf);
pf = NULL;
- 示例 2:向屏幕输出字符串
fputs("This is a test", stdout); // 屏幕输出"This is a test"(无换行)
fputs("\nAnother line", stdout); // 换行后输出"Another line"
4.4 文本行输入函数 fgets
-
函数原型:
char* fgets(char* str, int num, FILE* stream);
-
功能:从流(
stream
)中读取最多num-1
个字符(预留1个位置给\0
),遇到换行符(\n
)或文件末尾则停止。 -
参数:
str
为存储读取内容的缓冲区;num
为最大读取字符数(含\0
);stream
为文件指针或标准输入流。 -
返回值:成功返回
str
(缓冲区地址),失败或读到文件末尾返回NULL
。 -
示例1:从文件读取字符串(限制长度)
FILE* pf = fopen("test.txt", "r");
if (pf == NULL) {
perror("fopen failed");
return 1;
}
char buf[5]; // 最多读取4个字符(+1个\0)
fgets(buf, 5, pf); // 假设文件内容为"abcdef"
printf("%s\n", buf); // 输出"abcd"(仅读4个字符)
fclose(pf);
pf = NULL;
- 示例 2:从键盘读取一行(含换行符)
char buf[100];
printf("请输入:");
fgets(buf, 100, stdin); // 读取键盘输入(如输入"hello"并回车)
printf("你输入的是:%s", buf); // 输出"hello\n"(包含换行符)
4.5 格式化输出函数 fprintf
-
函数原型:
int fprintf(FILE* stream, const char* format, ...);
-
功能:按指定格式(
format
)将数据写入流(stream
),与printf
的区别是printf
固定输出到stdout
(屏幕),而fprintf
可输出到任意流(如文件)。 -
参数:
stream
为文件指针或标准流;format
为格式控制字符串;后续参数为要输出的数据。 -
返回值:成功返回写入的字符数,失败返回负数。
-
示例:向文件写入格式化数据
FILE* pf = fopen("user.txt", "w");
if (pf == NULL) {
perror("fopen failed");
return 1;
}
char name[] = "Alice";
int age = 25;
float score = 95.5;
// 按格式写入文件
fprintf(pf, "Name: %s, Age: %d, Score: %.1f\n", name, age, score);
// 文件内容:Name: Alice, Age: 25, Score: 95.5
fclose(pf);
pf = NULL;
对比 printf:printf("Name: %s\n", name)
等价于fprintf(stdout, "Name: %s\n", name)
。
4.6 格式化输入函数 fscanf
-
函数原型:
int fscanf(FILE* stream, const char* format, ...);
-
功能:按指定格式(
format
)从流(stream
)中读取数据,与scanf
的区别是scanf
固定从stdin
(键盘)读取,而fscanf
可从任意流(如文件)读取。 -
参数:
stream
为文件指针或标准流;format
为格式控制字符串;后续参数为存储数据的地址。 -
返回值:成功返回读取的项目数,失败或读到文件末尾返回
EOF
。 -
示例:从文件读取格式化数据
FILE* pf = fopen("user.txt", "r");
if (pf == NULL) {
perror("fopen failed");
return 1;
}
char name[20];
int age;
float score;
// 按格式读取文件内容(假设文件为"Name: Alice, Age: 25, Score: 95.5")
fscanf(pf, "Name: %s, Age: %d, Score: %f", name, &age, &score);
printf("解析结果:%s, %d, %.1f\n", name, age, score); // 输出"Alice, 25, 95.5"
fclose(pf);
pf = NULL;
4.7 标准流的默认打开
-
C程序启动时会自动打开3个标准流,无需手动
fopen
:stdin
:标准输入流,对应输入设备(通常是键盘),可作为fgetc
、fscanf
等函数的stream
参数。stdout
:标准输出流,对应输出设备(通常是屏幕),可作为fputc
、fprintf
等函数的stream
参数。stderr
:标准错误流,对应输出设备(通常是屏幕),用于输出错误信息(如perror
的输出)。
-
示例:使用标准流替代文件指针
// 从键盘读取字符(stdin)
int ch = fgetc(stdin);
// 向屏幕输出字符(stdout)
fputc(ch, stdout);
// 格式化输出到屏幕(等价于printf)
fprintf(stdout, "Hello, %s\n", "World"); // 输出"Hello, World"
4.8 二进制输入函数 fread
-
函数原型:
size_t fread(void* ptr, size_t size, size_t count, FILE* stream);
-
功能:从流(
stream
)中读取二进制数据,存储到ptr
指向的缓冲区。 -
参数:
ptr
为接收数据的缓冲区;size
为每个数据项的字节数;count
为数据项个数;stream
为文件指针。 -
返回值:成功返回实际读取的完整数据项个数(小于等于
count
),失败或读到文件末尾返回0。 -
示例:读取二进制文件中的结构体数组
typedef struct {
int id;
char name[20];
} Student;
FILE* pf = fopen("students.dat", "rb"); // 二进制读模式
if (pf == NULL) {
perror("fopen failed");
return 1;
}
Student s[3];
// 读取3个Student类型数据(每个4+20=24字节)
size_t n = fread(s, sizeof(Student), 3, pf);
printf("成功读取%d个学生数据\n", n);
fclose(pf);
pf = NULL;
4.9 二进制输出函数 fwrite
-
函数原型:
size_t fwrite(const void* ptr, size_t size, size_t count, FILE* stream);
-
功能:将
ptr
指向的二进制数据按指定大小和个数写入流(stream
)。 -
参数:
ptr
为待写入数据的地址;size
为每个数据项的字节数;count
为数据项个数;stream
为文件指针。 -
返回值:成功返回实际写入的完整数据项个数(等于
count
,否则表示出错)。 -
示例:向二进制文件写入结构体数组
typedef struct {
int id;
char name[20];
} Student;
Student s[2] = {
{1, "Alice"},
{2, "Bob"}
};
FILE* pf = fopen("students.dat", "wb"); // 二进制写模式
if (pf == NULL) {
perror("fopen failed");
return 1;
}
// 写入2个Student类型数据(每个24字节)
size_t n = fwrite(s, sizeof(Student), 2, pf);
if (n == 2) {
printf("成功写入2个学生数据\n");
}
fclose(pf);
pf = NULL;
5.文件的随机读写
5.1 fseek函数
-
函数原型:
int fseek(FILE* stream, long int offset, int origin);
-
功能:根据指定的起始位置(
origin
)和偏移量(offset
),移动文件指针到目标位置,实现随机读写(非顺序读写)。 -
参数详解:
stream
:文件指针(需已打开的文件)。offset
:偏移量(可正可负),正数表示向文件末尾方向移动,负数表示向文件起始方向移动。origin
:起始位置,有三种取值:SEEK_SET
:以文件开头为起始点( offset 为相对于文件开头的偏移量)。SEEK_CUR
:以文件指针当前位置为起始点( offset 为相对于当前位置的偏移量)。SEEK_END
:以文件末尾为起始点( offset 为相对于文件末尾的偏移量)。
-
返回值:成功返回0,失败返回非0。
-
示例1:从文件指定位置读取内容
FILE* pf = fopen("test.txt", "r");
if (pf == NULL) {
perror("fopen failed");
return 1;
}
// 假设文件内容为"abcdefghij"(共10个字符)
// 1. 从文件开头偏移3个字符(指向第4个字符'd')
fseek(pf, 3, SEEK_SET);
int ch1 = fgetc(pf);
printf("偏移后读取的字符:%c\n", ch1); // 输出'd'
// 2. 从当前位置('d'后)偏移2个字符(指向'f')
fseek(pf, 2, SEEK_CUR);
int ch2 = fgetc(pf);
printf("再次偏移后读取的字符:%c\n", ch2); // 输出'f'
// 3. 从文件末尾偏移-2个字符(指向'i')
fseek(pf, -2, SEEK_END);
int ch3 = fgetc(pf);
printf("从末尾偏移后读取的字符:%c\n", ch3); // 输出'i'
fclose(pf);
pf = NULL;
- 示例 2:在文件指定位置写入内容
FILE* pf = fopen("test.txt", "r+"); // 读写模式(不覆盖原有内容)
if (pf == NULL) {
perror("fopen failed");
return 1;
}
// 原文件内容:"abcdef"
fseek(pf, 2, SEEK_SET); // 定位到第3个字符'c'的位置
fputc('X', pf); // 将'c'替换为'X',文件内容变为"abXdef"
fclose(pf);
pf = NULL;
5.2 ftell函数
-
函数原型:
long int ftell(FILE* stream);
-
功能:计算文件指针当前位置相对于文件起始位置的偏移量(以字节为单位),用于获取当前读写位置。
-
参数:
stream
为文件指针。 -
返回值:成功返回当前偏移量(正数),失败返回-1L。
-
示例:结合fseek使用,获取偏移量
FILE* pf = fopen("test.txt", "r");
if (pf == NULL) {
perror("fopen failed");
return 1;
}
// 移动文件指针到第5个字符(偏移量为4,从0开始计数)
fseek(pf, 4, SEEK_SET);
// 获取当前偏移量
long int pos = ftell(pf);
printf("当前文件指针位置:%ld\n", pos); // 输出4(相对于文件开头偏移4字节)
fclose(pf);
pf = NULL;
- 应用场景:在使用 fseek 后,可通过 ftell 验证指针是否移动到预期位置;也可用于计算文件长度(fseek 到文件末尾,再用 ftell 获取偏移量)。
5.3 rewind函数
-
函数原型:
void rewind(FILE* stream);
-
功能:将文件指针重新定位到文件的起始位置,相当于
fseek(stream, 0, SEEK_SET);
,但无返回值。 -
参数:
stream
为文件指针。 -
示例:重置文件指针位置
FILE* pf = fopen("test.txt", "r");
if (pf == NULL) {
perror("fopen failed");
return 1;
}
// 读取文件前5个字符
char buf[6];
fread(buf, 1, 5, pf);
buf[5] = '\0';
printf("前5个字符:%s\n", buf); // 输出前5个字符
// 将文件指针重置到起始位置
rewind(pf);
// 重新读取文件前3个字符
fread(buf, 1, 3, pf);
buf[3] = '\0';
printf("重新读取前3个字符:%s\n", buf); // 输出前3个字符
fclose(pf);
pf = NULL;
- 优势: 比fseek更简洁,专门用于将指针复位到文件开头,无需指定偏移量和起始位置。
6. 文本文件和二进制文件
- 数据文件按数据的组织形式可分为文本文件和二进制文件,核心区别在于数据在外存(如磁盘)中的存储形式是否经过ASCII转换。
6.1 文本文件
- 定义:数据在外存中以ASCII码形式存储,数值型数据会先转换为对应的ASCII字符序列再存储。
- 特点:
- 可读性强(可用文本编辑器直接查看内容)。
- 存储数值型数据时占用空间较大(每个数字字符占1字节)。
- 示例:整数10000的文本存储
- 10000由字符’1’、‘0’、‘0’、‘0’、'0’组成,每个字符对应ASCII码0x31、0x30、0x30、0x30、0x30。
- 存储占用5字节,用文本编辑器打开可直接看到"10000"。
6.2 二进制文件
- 定义:数据在外存中以内存中的二进制形式直接存储,不经过ASCII转换(与数据在内存中的存储形式一致)。
- 特点:
- 可读性差(用文本编辑器打开显示乱码)。
- 存储高效,占用空间小(数值型数据按其实际字节数存储)。
- 示例:整数10000的二进制存储
- 在32位系统中,int类型的10000在内存中存储为二进制
0010011100010000
(十六进制0x2710),占用4字节。 - 用
fwrite
写入文件后,文件中直接存储这4字节,用文本编辑器打开无法识别(显示乱码)。
- 在32位系统中,int类型的10000在内存中存储为二进制
6.3 代码验证:文本与二进制存储的差异
#include <stdio.h>
int main() {
int a = 10000;
// 二进制存储(直接写入内存中的二进制形式)
FILE* pf_bin = fopen("binary.dat", "wb");
if (pf_bin != NULL) {
fwrite(&a, sizeof(int), 1, pf_bin); // 写入4字节
fclose(pf_bin);
pf_bin = NULL;
}
// 文本存储(先转换为ASCII码)
FILE* pf_txt = fopen("text.txt", "w");
if (pf_txt != NULL) {
fprintf(pf_txt, "%d", a); // 写入"10000",5字节
fclose(pf_txt);
pf_txt = NULL;
}
return 0;
}
- 结果对比:
binary.dat
大小为4
字节(二进制存储),text.txt
大小为5
字节(文本存储)。
用文本编辑器打开text.txt
可看到"10000"
,打开binary.dat
显示乱码。
7. 文件读取结束的判定
7.1 正确理解与使用feof函数
- 函数原型:
int feof(FILE* stream);
- 功能:用于文件读取结束后,判断结束原因:是正常读到文件末尾(返回非0),还是因读取错误(如磁盘错误)而结束(返回0)。
- 核心误区:
feof
不能直接用于判断文件是否结束(如循环条件中用!feof(pf)
是错误的),它仅用于结束后的原因判断。
7.2 文本文件读取结束的判定
-
判定依据:通过读取函数的返回值判断,而非
feof
。fgetc
:读取结束时返回EOF
(可直接作为判定条件)。fgets
:读取结束时返回NULL
(可直接作为判定条件)。
-
示例:用fgetc读取文本文件并判断结束
FILE* pf = fopen("test.txt", "r");
if (pf == NULL) {
perror("fopen failed");
return 1;
}
int ch;
// 循环读取,直到fgetc返回EOF(正常结束)
while ((ch = fgetc(pf)) != EOF) {
putchar(ch); // 输出读取的字符
}
// 读取结束后,用feof判断原因
if (feof(pf)) {
printf("\n正常结束:已读到文件末尾\n");
} else {
printf("\n异常结束:读取过程中发生错误\n");
}
fclose(pf);
pf = NULL;
- 示例:用 fgets 读取文本文件并判断结束
FILE* pf = fopen("test.txt", "r");
if (pf == NULL) {
perror("fopen failed");
return 1;
}
char buf[1024];
// 循环读取,直到fgets返回NULL(正常结束)
while (fgets(buf, sizeof(buf), pf) != NULL) {
fputs(buf, stdout); // 输出读取的行
}
// 读取结束后,用feof判断原因
if (feof(pf)) {
printf("\n正常结束:已读到文件末尾\n");
} else {
printf("\n异常结束:读取过程中发生错误\n");
}
fclose(pf);
pf = NULL;
7.3 二进制文件读取结束的判定
-
判定依据:通过
fread
的返回值判断,若返回值小于实际请求读取的个数,说明读取结束。 -
fread
返回值:成功返回实际读取的完整数据项个数,失败或读到文件末尾返回0(或小于请求个数)。 -
示例:用fread读取二进制文件并判断结束
typedef struct {
int id;
char name[20];
} Student;
FILE* pf = fopen("students.dat", "rb");
if (pf == NULL) {
perror("fopen failed");
return 1;
}
Student s;
size_t req = 1; // 每次请求读取1个Student
size_t n;
// 循环读取,直到fread返回值小于请求个数
while ((n = fread(&s, sizeof(Student), req, pf)) == req) {
// 处理读取到的学生数据
printf("ID: %d, Name: %s\n", s.id, s.name);
}
// 读取结束后,用feof判断原因
if (feof(pf)) {
printf("正常结束:已读完所有学生数据\n");
} else if (ferror(pf)) { // 配合ferror判断是否为错误结束
perror("读取错误");
}
fclose(pf);
pf = NULL;
说明:ferror(pf)用于判断是否因错误导致读取结束(如磁盘损坏),返回非 0 表示有错误。
8. 文件缓冲区
- 定义:缓冲文件系统中,系统会为每个正在使用的文件在内存中自动开辟一块文件缓冲区,用于临时存储读写数据。
- 工作机制:
- 写操作:数据先写入缓冲区,当缓冲区满、调用
fflush
或fclose
时,才将缓冲区数据一次性写入磁盘。 - 读操作:数据先从磁盘读到缓冲区,当缓冲区数据被程序读取完后,再从磁盘读取新数据填满缓冲区。
- 写操作:数据先写入缓冲区,当缓冲区满、调用
- 缓冲区大小:由C编译系统决定(通常为512字节或4096字节)。
8.1 缓冲区的影响与刷新
fflush
函数:int fflush(FILE* stream);
,强制将缓冲区中的数据写入磁盘(写操作)或清空缓冲区(读操作),无缓冲时无效果。- 示例:验证缓冲区存在
#include <stdio.h>
#include <unistd.h> // 包含sleep函数(Linux系统)
int main() {
FILE* pf = fopen("test.txt", "w");
if (pf == NULL) {
perror("fopen failed");
return 1;
}
fputs("Hello", pf); // 数据写入缓冲区(未立即写入磁盘)
printf("等待5秒...\n");
sleep(5); // 此时查看test.txt,内容为空(数据在缓冲区)
fflush(pf); // 刷新缓冲区,数据写入磁盘
printf("刷新缓冲区后,等待5秒...\n");
sleep(5); // 此时查看test.txt,内容为"Hello"
fclose(pf); // 关闭文件时也会刷新缓冲区
pf = NULL;
return 0;
}
说明:若不调用fflush或fclose,程序异常退出时,缓冲区数据可能丢失(未写入磁盘)。
8.2 缓冲区相关注意事项
-
必须刷新或关闭文件:因缓冲区存在,写文件后需调用
fflush
刷新或fclose
关闭文件,否则可能导致数据未写入磁盘。FILE* pf = fopen("test.txt", "w"); fputs("Hello", pf); // 若此时程序崩溃,"Hello"可能未写入磁盘(仍在缓冲区) fclose(pf); // 关闭时刷新,确保数据写入
-
fclose
与fflush
的区别:
fflush
:仅刷新缓冲区,不关闭文件,可继续操作文件。fclose
:先刷新缓冲区,再关闭文件,之后不可再操作文件。
- 标准输出流的特殊性:
stdout
(屏幕输出)通常是行缓冲,遇到\n
时自动刷新,而文件流是全缓冲(满缓冲才刷新)。
printf("Hello"); // 可能在缓冲区(未显示)
printf("World\n"); // 遇到\n,刷新缓冲区,显示"HelloWorld"