十:文件操作——让程序读写硬盘上的数据

一、回顾与本篇目标

上一篇你学会了动态内存分配——用 malloc 在堆上申请内存,用 free 释放内存。你的程序现在可以在运行时根据需要动态地创建和销毁数据了。

但还有一个问题没有解决:程序退出后,所有数据都没了。变量存在内存里,内存是临时存储——关机、程序退出,内存中的数据就消失了。如果你写了一个学生管理系统,录入了 50 个学生的信息,程序一关,这些数据就全丢了。下次再打开,还得重新输入。

数据需要持久化——存到硬盘上。硬盘上的数据不会因为关机而消失。这就是本篇要学的文件操作:让程序能够读写硬盘上的文件。

如果你之前跟过《Python 零基础入门》的文件读写篇,或者《后端零基础入门》中 Node.js 的 fs 模块,这一篇的概念你会觉得很熟悉——打开文件、读写内容、关闭文件,核心流程完全一样,只是 C 语言的语法更底层、更需要你手动管理细节。

本篇的目标:

  1. 理解 C 语言中文件操作的基本流程
  2. 学会用 fopen 打开文件和 fclose 关闭文件
  3. 掌握文本文件的读写:fprintffscanffgetsfputs
  4. 学会二进制文件的读写:freadfwrite
  5. 能把结构体数据保存到文件并读取出来

二、文件操作的基本流程

无论什么编程语言,文件操作都遵循同一个流程:

打开文件 → 读写文件 → 关闭文件

C 语言通过一个叫 FILE 的结构体来操作文件。FILE 是 C 标准库中定义的类型,包含了文件的所有信息——当前读写位置、缓冲区、文件状态等。你不需要知道 FILE 内部长什么样,只需要通过文件指针 FILE* 来操作它。

#include <stdio.h>

int main() {
    FILE *file;  // 文件指针

    // 1. 打开文件
    file = fopen("data.txt", "w");

    // 2. 检查是否打开成功
    if (file == NULL) {
        printf("无法打开文件!\n");
        return 1;
    }

    // 3. 写入数据
    fprintf(file, "你好,文件!\n");

    // 4. 关闭文件
    fclose(file);

    printf("数据已写入文件。\n");
    return 0;
}

逐行解释:

  • FILE *file:声明一个文件指针。它用来指向 fopen 返回的 FILE 对象。
  • fopen("data.txt", "w"):打开(或创建)一个名为 data.txt 的文件。"w" 表示“写入模式”。fopen 返回一个 FILE*,如果打开失败(比如没有权限、磁盘满了),返回 NULL
  • if (file == NULL)必须检查 fopen 的返回值。不检查就直接使用 file,如果打开失败会导致程序崩溃。
  • fprintf(file, "你好,文件!\n"):向文件写入内容。fprintf 的用法和 printf 几乎一样,只是多了一个 FILE* 参数指定写到哪个文件。
  • fclose(file):关闭文件。文件用完后必须关闭。关闭会做两件事:把缓冲区中还没写入硬盘的数据真正写进去,释放系统资源。不关闭可能导致数据丢失或资源泄漏。

三、fopen 的打开模式

fopen 的第二个参数决定了你以什么方式打开文件:

模式 含义 文件不存在时 文件存在时
"r" 只读 返回 NULL(报错) 从文件开头读取
"w" 写入(覆盖) 创建新文件 清空原内容
"a" 追加 创建新文件 在文件末尾追加
"r+" 读写 返回 NULL(报错) 从文件开头读写
"w+" 读写(覆盖) 创建新文件 清空原内容
"a+" 读取+追加 创建新文件 读取从头开始,写入在末尾
"rb", "wb", "ab" 二进制模式 和对应的文本模式相同,但以二进制方式操作

最常用的三个模式"r" 读、"w" 写、"a" 追加。"w" 模式需要特别小心——它会清空文件原有内容。如果只是想添加内容,用 "a"

文本模式和二进制模式的区别

  • 文本模式(不带 b):会对换行符做转换。Windows 上 \n 会被转成 \r\n 写入,读取时再转回来。
  • 二进制模式(带 b):不做任何转换,原样写入、原样读出。适合处理图片、音频、视频等非文本数据。

四、文本文件的读写

4.1 写入文本:fprintf

fprintf 的用法和 printf 完全一样,只是第一个参数是 FILE*

FILE *file = fopen("data.txt", "w");
if (file == NULL) return 1;

int age = 28;
float score = 92.5;
char name[] = "张三";

// 像使用 printf 一样向文件写入
fprintf(file, "姓名:%s\n", name);
fprintf(file, "年龄:%d\n", age);
fprintf(file, "成绩:%.1f\n", score);

fclose(file);

生成的 data.txt 文件内容:

姓名:张三
年龄:28
成绩:92.5

4.2 读取文本:fscanf

fscanf 的用法和 scanf 完全一样,只是第一个参数是 FILE*

FILE *file = fopen("data.txt", "r");
if (file == NULL) return 1;

char name[50];
int age;
float score;

// 从文件读取格式化的数据
fscanf(file, "姓名:%s\n", name);
fscanf(file, "年龄:%d\n", &age);
fscanf(file, "成绩:%f\n", &score);

printf("读取到:%s,%d 岁,%.1f 分\n", name, age, score);

fclose(file);

fscanf 的局限:它对格式要求很严格。如果文件内容的格式和你写的格式串不完全一致,读取就会失败。对于结构不固定的文本文件,通常用 fgets 更灵活。

4.3 逐行读取:fgets

fgets 从文件中读取一行(包括换行符),存入字符串:

FILE *file = fopen("data.txt", "r");
if (file == NULL) return 1;

char line[256];

// 逐行读取,直到文件结束
while (fgets(line, sizeof(line), file) != NULL) {
    printf("读到一行:%s", line);  // line 本身包含换行符,所以不加 \n
}

fclose(file);

fgets 的参数:

  • line:存储读取内容的字符数组。
  • sizeof(line):最多读取的字符数(包括 \0)。
  • file:文件指针。
  • 返回值:成功返回 line,读到文件末尾或出错返回 NULL

4.4 写入字符串:fputs

FILE *file = fopen("output.txt", "w");
if (file == NULL) return 1;

fputs("第一行\n", file);
fputs("第二行\n", file);
fputs("第三行\n", file);

fclose(file);

fputs 写入一个字符串到文件,不会自动加换行符(需要手动加 \n)。

五、二进制文件的读写

文本文件适合人类直接阅读的数据。但对于结构化的数据(比如结构体数组),用二进制方式存储更高效——不需要在文本和二进制之间转换,直接按内存中的原始格式读写。

5.1 写入二进制:fwrite

#include <stdio.h>

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

int main() {
    Student students[3] = {
        {"张三", 20, 85.5},
        {"李四", 21, 92.0},
        {"王五", 19, 78.5}
    };

    FILE *file = fopen("students.bin", "wb");
    if (file == NULL) return 1;

    // fwrite(数据地址, 每个元素的大小, 元素个数, 文件指针)
    size_t written = fwrite(students, sizeof(Student), 3, file);
    printf("写入了 %zu 个学生记录\n", written);

    fclose(file);
    return 0;
}

fwrite 的参数:

  • 第一个参数:要写入的数据的内存地址。
  • 第二个参数:每个元素的大小(字节)。
  • 第三个参数:要写入的元素个数。
  • 第四个参数:文件指针。
  • 返回值:实际成功写入的元素个数。

"wb" 模式打开——w 表示写入,b 表示二进制模式。

5.2 读取二进制:fread

#include <stdio.h>

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

int main() {
    Student students[3];

    FILE *file = fopen("students.bin", "rb");
    if (file == NULL) return 1;

    // fread(存储位置, 每个元素的大小, 要读取的元素个数, 文件指针)
    size_t read_count = fread(students, sizeof(Student), 3, file);
    printf("读入了 %zu 个学生记录\n", read_count);

    fclose(file);

    // 输出读取到的数据
    for (size_t i = 0; i < read_count; i++) {
        printf("%s,%d 岁,%.1f 分\n",
               students[i].name, students[i].age, students[i].score);
    }

    return 0;
}

fread 的参数和 fwrite 完全对称,只是数据方向相反——fread 从文件读到内存,fwrite 从内存写到文件。

注意:用文本编辑器打开 students.bin 会看到乱码——因为它是二进制格式,不是给人读的。但程序可以精确地读写它。

六、其他文件操作函数

6.1 feof:判断是否读到文件末尾

在循环读取文件时,可以用 feof 判断是否读到了文件末尾:

FILE *file = fopen("data.txt", "r");
char ch;

while ((ch = fgetc(file)) != EOF) {
    putchar(ch);
}

// 或者用 feof
// while (!feof(file)) { ... }

不过,更推荐的做法是检查读取函数的返回值(如 fgets 返回 NULL 表示结束),而不是依赖 feoffeof 只在尝试读取之后才会被设置,有时会导致多读一次。

6.2 fseek 和 ftell:在文件中定位

fseek 移动文件内部的“读写位置指针”,ftell 获取当前位置:

// 把位置移到文件末尾
fseek(file, 0, SEEK_END);
long file_size = ftell(file);
printf("文件大小:%ld 字节\n", file_size);

// 把位置移回文件开头
fseek(file, 0, SEEK_SET);

fseek 的第三个参数:

  • SEEK_SET:从文件开头开始计算偏移。
  • SEEK_CUR:从当前位置开始计算偏移。
  • SEEK_END:从文件末尾开始计算偏移。

6.3 remove:删除文件

if (remove("temp.txt") == 0) {
    printf("文件删除成功\n");
} else {
    printf("文件删除失败\n");
}

6.4 rename:重命名文件

if (rename("old_name.txt", "new_name.txt") == 0) {
    printf("重命名成功\n");
}

七、和 Python/Node.js 文件操作的对比

如果你之前学过 Python 或 Node.js 的文件操作,这张表能帮你快速迁移:

操作 Python Node.js C
打开文件 open("file.txt", "w") fs.openSync("file.txt", "w") fopen("file.txt", "w")
写入文本 file.write("hello") fs.writeFileSync(...) fprintf(file, "hello")
读取一行 file.readline() readline 模块 fgets(buffer, size, file)
关闭文件 file.close()with fs.closeSync(fd) fclose(file)
自动关闭 with open(...) as f: 回调/Promise 自动管理 没有!必须手动 fclose
删除文件 os.remove("file.txt") fs.unlinkSync(...) remove("file.txt")

C 语言的文件操作最“原始”,没有 Python 的 with 语句(自动关闭文件),没有 Node.js 的异步回调。你必须自己管理文件指针,自己保证 fclose 被调用。

八、综合演示:学生信息的持久化存储

下面这个示例把结构体和文件操作结合起来,实现一个简单的学生信息管理系统,数据保存到硬盘上:

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

#define MAX_STUDENTS 100
#define FILENAME "students.dat"

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

// 保存学生数据到文件
int save_students(const Student *students, int count) {
    FILE *file = fopen(FILENAME, "wb");
    if (file == NULL) {
        printf("无法打开文件进行写入!\n");
        return 0;
    }

    // 先写入学生数量
    fwrite(&count, sizeof(int), 1, file);
    // 再写入所有学生数据
    fwrite(students, sizeof(Student), count, file);

    fclose(file);
    return 1;
}

// 从文件读取学生数据
int load_students(Student *students) {
    FILE *file = fopen(FILENAME, "rb");
    if (file == NULL) {
        // 文件不存在,返回 0 表示没有数据
        return 0;
    }

    int count;
    // 先读取学生数量
    if (fread(&count, sizeof(int), 1, file) != 1) {
        fclose(file);
        return 0;
    }

    if (count > MAX_STUDENTS) count = MAX_STUDENTS;

    // 再读取所有学生数据
    size_t read_count = fread(students, sizeof(Student), count, file);
    fclose(file);

    return (int)read_count;
}

int main() {
    Student students[MAX_STUDENTS];
    int count = 0;

    // 尝试从文件加载已有数据
    count = load_students(students);
    if (count > 0) {
        printf("从文件加载了 %d 个学生的数据\n\n", count);
        for (int i = 0; i < count; i++) {
            printf("%d. %s,%d 岁,%.1f 分\n",
                   i + 1, students[i].name, students[i].age, students[i].score);
        }
    } else {
        printf("没有找到已有数据,开始录入新数据\n\n");
    }

    // 添加新学生
    printf("\n===== 添加学生 =====\n");
    printf("输入 'quit' 结束录入\n");

    while (count < MAX_STUDENTS) {
        char input[50];
        printf("\n姓名:");
        scanf("%s", input);

        if (strcmp(input, "quit") == 0) break;

        strcpy(students[count].name, input);
        printf("年龄:");
        scanf("%d", &students[count].age);
        printf("成绩:");
        scanf("%f", &students[count].score);

        count++;
    }

    // 保存到文件
    if (save_students(students, count)) {
        printf("\n数据已保存到 %s(共 %d 条记录)\n", FILENAME, count);
    } else {
        printf("\n数据保存失败!\n");
    }

    return 0;
}

代码设计要点:

  • 二进制存储:用 fwritefread 直接存储结构体数组。这比用文本格式更简洁——不需要为每个字段写格式化代码。
  • 先存数量:文件开头存储了学生数量,读取时先读数量,再根据数量读取对应条记录。这样不需要遍历文件或猜测有多少数据。
  • 持久化:程序退出后,数据保存在 students.dat 文件中。再次运行程序,load_students 会自动加载已有数据,之前的录入不会丢失。
  • 文件名用宏定义#define FILENAME "students.dat",修改文件名时只需改一处。

九、本篇动手练习

练习 1:文本文件复制

新建 practice10-1.c,实现文本文件复制功能。从 source.txt 逐字符读取,写入 destination.txt。提示:用 fgetcfputc,或者用 fgetsfputs

练习 2:统计文件行数

新建 practice10-2.c,读取一个文本文件,统计文件中有多少行、多少个字符(不包括换行符)。

练习 3:图书信息持久化

新建 practice10-3.c,定义 Book 结构体(书名、作者、价格、页数)。实现添加图书、显示所有图书、保存到二进制文件、从文件加载的功能。

练习 4:日志记录器

新建 practice10-4.c,写一个 log_message 函数,每次调用时把当前时间和一条消息追加到 app.log 文件中。提示:获取当前时间用 time()ctime()(需要 time.h),打开模式用 "a"

十、本篇小结

这一篇你学会了 C 语言中文件操作的核心知识:

  • 文件操作流程fopen 打开 → 读写 → fclose 关闭。文件用完后必须关闭。
  • fopen 的打开模式"r" 只读、"w" 覆盖写入、"a" 追加写入。"b" 后缀表示二进制模式。
  • 文本读写fprintf/fscanf 格式化读写,fgets/fputs 逐行读写。
  • 二进制读写fwrite/fread 直接按内存格式读写,适合结构体等结构化数据。
  • 文件定位fseek 移动读写位置,ftell 获取当前位置。
  • 文件管理remove 删除文件,rename 重命名文件。

文件操作让你的程序有了“记忆力”——数据可以持久化到硬盘上,下次运行时还能读出来。这是从“玩具程序”到“实用工具”的关键一步。下一篇,我们学习 C 语言的预处理指令——#include#define、条件编译等。这些以 # 开头的指令在编译之前执行,是 C 语言独特的“元编程”能力。

下一篇预告

下一篇——《预处理指令——编译之前的代码处理》:#include 的本质(复制粘贴)、#define 宏定义和宏函数、条件编译(#ifdef#ifndef)、头文件保护(防止重复包含)。理解这些能让你看懂大型 C 项目的代码组织方式。

C/C++ 零基础入门,每周更新。

© 版权声明
THE END
喜欢就支持一下吧
点赞15 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容