C语言文件操作

程序运行时产生的数据通常存储在内存中,一旦程序结束,这些数据就会丢失。 文件操作允许我们将数据持久化保存到磁盘文件中,并在需要时读取出来。 C语言通过标准库提供了一组文件操作函数,支持文本文件和二进制文件的读写。 本章将系统讲解C语言中文件操作的相关函数及使用方法。

📂 文件指针与文件打开模式

在C语言中,使用 FILE 类型的指针来操作文件。FILE 是一个结构体类型,在 <stdio.h> 中定义, 它包含了文件的各种信息,如文件位置指示器、缓冲区状态等。我们通过 fopen() 函数打开文件并获取文件指针。

FILE *fopen(const char *filename, const char *mode);

mode 参数指定文件的打开模式:

3232 3232 3232 3232 3232 3232 3232 3232
模式说明文件存在文件不存在
"r"只读(文本)正常打开打开失败,返回NULL"w"只写(文本)内容被清空创建新文件"a"追加(文本)在文件末尾追加创建新文件"r+"读写(文本)正常打开打开失败"w+"读写(文本)内容被清空创建新文件"a+"读和追加(文本)可在末尾追加,可读创建新文件"rb", "wb", "ab"二进制模式同上(b表示二进制)同上
注意: 在Windows系统中,文本模式下换行符 \n 会被转换为 \r\n,二进制模式则不会进行转换。Unix/Linux系统下两者无区别。

🔹 打开文件示例

FILE *fp = fopen("test.txt", "r");
if (fp == NULL) {
    perror("打开文件失败");
    return 1;
}

🔒 关闭文件:fclose

文件操作完成后,必须调用 fclose() 关闭文件,释放资源并确保缓冲区数据写入磁盘。

int fclose(FILE *stream);
if (fclose(fp) != 0) {
    printf("关闭文件失败\n");
}

📝 文本文件读写

🔸 格式化读写:fprintf / fscanf

fprintffscanfprintf/scanf 类似,只是将输入输出目标改为文件。

#include <stdio.h>

int main() {
    FILE *fp = fopen("data.txt", "w");
    if (fp == NULL) {
        perror("打开文件失败");
        return 1;
    }

    // 写入数据
    fprintf(fp, "姓名: %s 年龄: %d 成绩: %.1f\n", "张三", 20, 88.5);
    fprintf(fp, "姓名: %s 年龄: %d 成绩: %.1f\n", "李四", 22, 92.0);
    fclose(fp);

    // 读取数据
    fp = fopen("data.txt", "r");
    if (fp == NULL) {
        perror("打开文件失败");
        return 1;
    }

    char name[50];
    int age;
    float score;
    while (fscanf(fp, "姓名: %s 年龄: %d 成绩: %f\n", name, &age, &score) == 3) {
        printf("%s, %d, %.1f\n", name, age, score);
    }
    fclose(fp);

    return 0;
}

🔸 字符读写:fputc / fgetc

fputc 写入一个字符,fgetc 读取一个字符,常用于逐字符处理。

#include <stdio.h>

int main() {
    // 写入文件
    FILE *fp = fopen("chars.txt", "w");
    if (fp) {
        char str[] = "Hello, C Language!";
        for (int i = 0; str[i] != '\0'; i++) {
            fputc(str[i], fp);
        }
        fclose(fp);
    }

    // 读取并输出
    fp = fopen("chars.txt", "r");
    if (fp) {
        int ch;
        while ((ch = fgetc(fp)) != EOF) {
            putchar(ch);
        }
        putchar('\n');
        fclose(fp);
    }
    return 0;
}
注意: fgetc 返回 int 类型,当读到文件末尾或出错时返回 EOF(通常为-1)。

🔸 字符串读写:fputs / fgets

fputs 写入字符串(不自动添加换行),fgets 读取一行(包含换行符,除非行太长)。

#include <stdio.h>

int main() {
    FILE *fp = fopen("lines.txt", "w");
    if (fp) {
        fputs("第一行\n", fp);
        fputs("第二行\n", fp);
        fclose(fp);
    }

    fp = fopen("lines.txt", "r");
    if (fp) {
        char line[100];
        while (fgets(line, sizeof(line), fp) != NULL) {
            printf("读取: %s", line);  // 注意line中已包含换行符
        }
        fclose(fp);
    }
    return 0;
}
提示: fgets 读取时最多读取 size-1 个字符,并自动在末尾添加 '\0'。若遇到换行符,会将换行符也读入。

💾 二进制文件读写

二进制文件以字节流的方式存储数据,常用于保存结构体、数组等原始内存数据,读写速度更快。

🔸 块读写:fwrite / fread

size_t fwrite(const void *ptr, size_t size, size_t count, FILE *stream);
size_t fread(void *ptr, size_t size, size_t count, FILE *stream);

参数说明:

  • ptr:指向数据缓冲区的指针。
  • size:每个数据项的大小(字节数)。
  • count:要读写的数据项个数。
  • stream:文件指针。
  • 返回值:实际成功读写的完整数据项个数。
#include <stdio.h>

typedef struct {
    int id;
    char name[20];
    float score;
} Student;

int main() {
    Student stu[] = {
        {1001, "张三", 88.5},
        {1002, "李四", 92.0},
        {1003, "王五", 78.5}
    };
    int n = sizeof(stu) / sizeof(Student);

    // 写入二进制文件
    FILE *fp = fopen("students.dat", "wb");
    if (fp) {
        fwrite(stu, sizeof(Student), n, fp);
        fclose(fp);
    }

    // 读取二进制文件
    Student readStu[10];
    fp = fopen("students.dat", "rb");
    if (fp) {
        int count = fread(readStu, sizeof(Student), 10, fp);
        for (int i = 0; i < count; i++) {
            printf("%d\t%s\t%.1f\n", readStu[i].id, readStu[i].name, readStu[i].score);
        }
        fclose(fp);
    }
    return 0;
}

📍 文件定位:fseek、ftell、rewind

文件内部有一个位置指示器,记录当前读写位置。我们可以使用以下函数调整位置:

  • fseek(FILE *stream, long offset, int whence); —— 移动位置指示器。
  • ftell(FILE *stream); —— 获取当前位置(相对于文件开头的字节数)。
  • rewind(FILE *stream); —— 将位置指示器重置到文件开头。

whence 参数可取:

  • SEEK_SET:文件开头
  • SEEK_CUR:当前位置
  • SEEK_END:文件末尾
#include <stdio.h>

int main() {
    FILE *fp = fopen("test.txt", "w+");
    if (!fp) return 1;

    fprintf(fp, "0123456789");
    rewind(fp);          // 回到开头

    // 移动到第5个字节(索引5)
    fseek(fp, 5, SEEK_SET);
    int ch = fgetc(fp);
    printf("位置5的字符: %c\n", ch);  // 输出 '5'

    // 获取当前位置
    long pos = ftell(fp);
    printf("当前位置: %ld\n", pos);   // 输出 6

    // 从当前位置向后移动3个字节
    fseek(fp, 3, SEEK_CUR);
    ch = fgetc(fp);
    printf("移动后字符: %c\n", ch);   // 输出 '9'

    // 移动到文件末尾,并获取文件大小
    fseek(fp, 0, SEEK_END);
    long size = ftell(fp);
    printf("文件大小: %ld 字节\n", size);

    fclose(fp);
    return 0;
}

⚠️ 文件操作错误处理

文件操作极易出错,如文件不存在、权限不足、磁盘满等。应该始终检查函数返回值,并使用 perror()ferror() 定位问题。

#include <stdio.h>

int main() {
    FILE *fp = fopen("nonexist.txt", "r");
    if (fp == NULL) {
        perror("打开文件失败");   // 打印系统错误信息
        return 1;
    }

    // 读写过程中可以检查错误
    int ch = fgetc(fp);
    if (ferror(fp)) {
        printf("读取文件时发生错误\n");
        clearerr(fp);  // 清除错误标志
    }

    fclose(fp);
    return 0;
}

📋 综合示例:学生成绩管理系统(文件存储)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct {
    int id;
    char name[50];
    float score;
} Student;

void saveToFile(Student stu[], int count, const char *filename) {
    FILE *fp = fopen(filename, "wb");
    if (fp == NULL) {
        perror("保存文件失败");
        return;
    }
    fwrite(stu, sizeof(Student), count, fp);
    fclose(fp);
    printf("保存成功,共 %d 条记录\n", count);
}

int loadFromFile(Student stu[], int max, const char *filename) {
    FILE *fp = fopen(filename, "rb");
    if (fp == NULL) {
        printf("文件不存在或无法打开,将创建新文件\n");
        return 0;
    }
    int count = fread(stu, sizeof(Student), max, fp);
    fclose(fp);
    return count;
}

void addStudent(Student stu[], int *count) {
    if (*count >= 100) {
        printf("学生数量已达上限\n");
        return;
    }
    Student s;
    printf("请输入学号: ");
    scanf("%d", &s.id);
    printf("请输入姓名: ");
    scanf("%s", s.name);
    printf("请输入成绩: ");
    scanf("%f", &s.score);
    stu[*count] = s;
    (*count)++;
    printf("添加成功\n");
}

void displayStudents(Student stu[], int count) {
    if (count == 0) {
        printf("暂无学生信息\n");
        return;
    }
    printf("\n学号\t姓名\t成绩\n");
    for (int i = 0; i < count; i++) {
        printf("%d\t%s\t%.1f\n", stu[i].id, stu[i].name, stu[i].score);
    }
    printf("\n");
}

int main() {
    Student students[100];
    int count = 0;
    const char *filename = "students.dat";
    int choice;

    // 加载已有数据
    count = loadFromFile(students, 100, filename);
    printf("已加载 %d 条学生记录\n", count);

    do {
        printf("\n===== 学生成绩管理系统 =====\n");
        printf("1. 添加学生\n");
        printf("2. 查看所有学生\n");
        printf("3. 保存并退出\n");
        printf("请选择: ");
        scanf("%d", &choice);

        switch (choice) {
            case 1:
                addStudent(students, &count);
                break;
            case 2:
                displayStudents(students, count);
                break;
            case 3:
                saveToFile(students, count, filename);
                printf("退出系统\n");
                break;
            default:
                printf("无效选择\n");
        }
    } while (choice != 3);

    return 0;
}

⚠️ 常见错误与注意事项

  • 忘记关闭文件:导致资源泄漏,缓冲区数据可能丢失。
  • 未检查 fopen 返回值:直接使用 NULL 指针导致程序崩溃。
  • 读写模式不匹配:以 "r" 模式打开文件后尝试写入,会导致错误。
  • 文本模式下对换行符的误解:在Windows上,文本模式下读写时换行符会转换,二进制模式不会。
  • fread/fwrite 返回值处理不当:可能未完整读写所有数据,应检查返回值。
  • 使用 fscanf 读取格式不匹配的文件:可能导致读取失败,应检查返回值。
  • 指针越界或缓冲区溢出:使用 fgets 时要确保缓冲区足够大。
  • 混淆 EOF 与字符值:当使用 fgetc 时,返回类型为 int,不能直接赋给 char 后比较 EOF,应使用 int 变量接收。

文件操作使得程序能够持久化保存数据,是开发实际应用程序的必备技能。通过掌握 fopenfclose、读写函数以及文件定位,你可以实现数据的存储与读取。 至此,C语言基础部分已全部结束。接下来的学习可以继续深入指针高级应用、数据结构等内容。