引言
文件操作是 C 语言系统编程的基础技能。无论是配置文件读写、数据持久化还是日志记录,都离不开文件 I/O。本文详细介绍 fopen、fclose、fread、fwrite、fprintf、fscanf 等函数的用法,并对比文本与二进制文件的操作差异。
一、文件打开与关闭
fopen 用于打开文件,fclose 用于关闭文件:
FILE *fopen(const char *filename, const char *mode);
// 打开模式
FILE *fp = fopen("data.txt", "r"); // 读文本
FILE *fp = fopen("data.txt", "w"); // 写文本(覆盖)
FILE *fp = fopen("data.txt", "a"); // 追加写
FILE *fp = fopen("data.bin", "rb"); // 读二进制
FILE *fp = fopen("data.bin", "wb"); // 写二进制
if (fp == NULL) {
perror("fopen failed");
return -1;
}
// 使用完毕后必须关闭
fclose(fp);
fopen 的 mode 参数直接影响文件的打开方式:
- "r" / "rb":文件必须存在,否则失败
- "w" / "wb":文件不存在则创建,存在则清空内容
- "a" / "ab":追加模式,文件不存在则创建
- "r" / "rb":文件必须存在,否则失败
- "w" / "wb":文件不存在则创建,存在则清空内容
- "a" / "ab":追加模式,文件不存在则创建
二、文本文件读写
2.1 fprintf 与 fscanf
类似于 printf/scanf,但多了文件指针参数:
// 写入文本
FILE *fp = fopen("test.txt", "w");
if (fp) {
fprintf(fp, "Name: %s, Age: %d, Score: %.2f\n", "Tom", 25, 92.5);
fprintf(fp, "Value: %d, Hex: 0x%X\n", 255, 255);
fclose(fp);
}
// 读取文本
fp = fopen("test.txt", "r");
char name[32];
int age;
float score;
int value, hex;
while (fscanf(fp, "Name: %s, Age: %d, Score: %f\n", name, &age, &score) == 3) {
printf("%s, %d, %.2f\n", name, age, score);
}
fscanf(fp, "Value: %d, Hex: 0x%x\n", &value, &hex);
printf("Value: %d, Hex: %d\n", value, hex);
fclose(fp);
fscanf 返回成功匹配并赋值的项数,可用于判断读取是否成功。
2.2 fgets 与 fputs
按行读取,更安全可靠:
// fgets:读取一行,保留换行符(如果有)
char line[256];
FILE *fp = fopen("test.txt", "r");
while (fgets(line, sizeof(line), fp) != NULL) {
printf("%s", line); // 本身已包含换行符
}
fclose(fp);
// fputs:写入一行,不自动加换行符
fp = fopen("output.txt", "w");
fputs("第一行\n", fp);
fputs("第二行\n", fp);
fclose(fp);
2.3 fgetc 与 fputc
读写单个字符:
// 读取整个文件(字符流方式)
int ch;
int line_count = 0;
FILE *fp = fopen("test.txt", "r");
while ((ch = fgetc(fp)) != EOF) {
putchar(ch);
if (ch == '\n') line_count++;
}
printf("Total lines: %d\n", line_count);
fclose(fp);
// 复制文件
int copy_file(const char *src, const char *dst) {
FILE *in = fopen(src, "rb");
FILE *out = fopen(dst, "wb");
int ch;
while ((ch = fgetc(in)) != EOF) {
fputc(ch, out);
}
fclose(in);
fclose(out);
return 0;
}
注意:fgetc 返回 int 而非 char,因为 EOF 通常定义为 -1,需要用 int 来区分有效字符。
三、二进制文件读写
二进制文件以原始字节形式存储数据,无格式转换,效率更高。
typedef struct {
int id;
char name[32];
double salary;
} Employee;
// 写入二进制
void write_employees(const char *filename) {
Employee emps[3] = {
{1, "Alice", 8000.0},
{2, "Bob", 9500.0},
{3, "Carol", 7200.0}
};
FILE *fp = fopen(filename, "wb");
fwrite(emps, sizeof(Employee), 3, fp);
fclose(fp);
}
// 读取二进制
void read_employees(const char *filename) {
Employee emp;
FILE *fp = fopen(filename, "rb");
while (fread(&emp, sizeof(Employee), 1, fp) == 1) {
printf("ID: %d, Name: %s, Salary: %.2f\n",
emp.id, emp.name, emp.salary);
}
fclose(fp);
}
fread/fwrite 的参数:
- 第1个:数据缓冲区地址
- 第2个:每个元素的大小
- 第3个:元素个数
- 第4个:文件指针
- 返回值:实际读写成功的元素个数
- 第1个:数据缓冲区地址
- 第2个:每个元素的大小
- 第3个:元素个数
- 第4个:文件指针
- 返回值:实际读写成功的元素个数
四、文件定位
使用 ftell、fseek、rewind 控制读写位置:
// 获取当前文件位置(相对于文件开头的字节偏移)
long pos = ftell(fp);
printf("Current position: %ld\n", pos);
// fseek:移动文件位置
fseek(fp, 0, SEEK_SET); // 回到文件开头
fseek(fp, 0, SEEK_END); // 跳到文件末尾
fseek(fp, 10, SEEK_CUR); // 从当前位置前进 10 字节
fseek(fp, -5, SEEK_END); // 从末尾后退 5 字节
// rewind:等价于 fseek(fp, 0, SEEK_SET)
rewind(fp);
// 获取文件大小
fseek(fp, 0, SEEK_END);
long file_size = ftell(fp);
rewind(fp);
五、文件状态与缓冲区
// feof:检查是否到达文件末尾
while (!feof(fp)) {
// 读取数据...
}
// ferror:检查文件操作是否出错
if (ferror(fp)) {
perror("File error");
}
// fflush:强制刷新缓冲区(确保数据写入磁盘)
fflush(fp); // 刷新输出缓冲区
// 设置缓冲区(可选)
char buf[8192];
setvbuf(fp, buf, _IOFBF, sizeof(buf)); // 全缓冲
setvbuf(fp, NULL, _IONBF, 0); // 无缓冲
setvbuf(fp, NULL, _IOLBF, 0); // 行缓冲(用于终端)
注意:feof 必须在尝试读取失败后才能判断,不要在循环开始前检查 feof。
六、完整示例:学生信息管理系统
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
int id;
char name[32];
int score;
} Student;
void add_student(const char *filename) {
Student s;
printf("ID: "); scanf("%d", &s.id);
printf("Name: "); scanf("%s", s.name);
printf("Score: "); scanf("%d", &s.score);
FILE *fp = fopen(filename, "ab");
fwrite(&s, sizeof(Student), 1, fp);
fclose(fp);
}
void list_students(const char *filename) {
Student s;
FILE *fp = fopen(filename, "rb");
if (!fp) { printf("No records yet.\n"); return; }
printf("%-6s %-10s %-6s\n", "ID", "Name", "Score");
printf("------------------------\n");
while (fread(&s, sizeof(Student), 1, fp) == 1) {
printf("%-6d %-10s %-6d\n", s.id, s.name, s.score);
}
fclose(fp);
}
int main(void) {
const char *file = "students.dat";
int choice;
while (1) {
printf("\n1.Add 2.List 0.Exit: ");
scanf("%d", &choice);
if (choice == 1) add_student(file);
else if (choice == 2) list_students(file);
else break;
}
return 0;
}
七、总结
- fopen/fclose:打开和关闭文件,始终检查返回值
- 文本模式:使用 fprintf、fscanf、fgets、fgetc 等
- 二进制模式:使用 fread、fwrite,无格式转换
- 文件定位:fseek 控制位置,ftell 获取偏移,rewind 回开头
- 安全检查:feof、ferror 检查状态,fflush 强制刷新
- 跨平台注意:Windows 下换行符不同,二进制文件要加 "b"