C 语言文件 I/O 完全指南

引言

文件操作是 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":追加模式,文件不存在则创建

二、文本文件读写

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个:文件指针
- 返回值:实际读写成功的元素个数

四、文件定位

使用 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"