C: 文件操作

文件就像一个保险柜——用之前要打开(fopen),取放东西要按规矩来(读写方式),用完必须锁好(fclose),不然东西可能丢了。

1. 文件的基本概念

C语言把所有外部设备都当作"流"来处理。文件流分为两种:

操作系统通过一个 FILE 结构体来管理打开的文件,我们通过 FILE * 指针来操作它。

(1) 文件指针

C
FILE *fp;

这个指针是后续所有文件操作的"钥匙"。没有它,什么都做不了。


2. 打开与关闭文件

(1) fopen

C
FILE *fopen(const char *filename, const char *mode);
模式 含义 文件不存在时 文件已存在时
"r" 只读 出错 从头读
"w" 只写 创建 清空内容
"a" 追加 创建 末尾追加
"r+" 读写 出错 从头操作
"w+" 读写 创建 清空内容
"a+" 读追加 创建 末尾追加
"rb" 二进制只读 出错 从头读
"wb" 二进制只写 创建 清空内容
"ab" 二进制追加 创建 末尾追加
💡 "w" 模式会清空已有文件!这是新手最常犯的错误——本想追加内容却用了 "w" 导致数据全丢了。

(2) fclose

C
int fclose(FILE *fp);

关闭文件会刷新缓冲区并释放资源。成功返回 0,失败返回 EOF

C
#include <stdio.h>

int main(void) {
    FILE *fp = fopen("data.txt", "w");
    if (fp == NULL) {
        printf("无法打开文件\n");
        return 1;
    }
    fprintf(fp, "Hello, File!\n");
    fclose(fp);
    return 0;
}
⚠️ 每一个 fopen 必须对应一个 fclose。忘记关闭文件会导致内存泄漏和数据丢失。


3. 文本文件读写

(1) 字符读写:fgetc / fputc

C
int fgetc(FILE *fp);
int fputc(int ch, FILE *fp);

fgetc 读取一个字符,到达文件末尾返回 EOFfputc 写入一个字符。

(2) 字符串读写:fgets / fputs

C
char *fgets(char *str, int n, FILE *fp);
int fputs(const char *str, FILE *fp);

fgets 最多读 n-1 个字符,遇到换行或文件末尾停止,自动添加 '\0'

C
#include <stdio.h>

int main(void) {
    FILE *fp = fopen("poem.txt", "w");
    if (fp == NULL) {
        return 1;
    }
    fputs("白日依山尽\n", fp);
    fputs("黄河入海流\n", fp);
    fclose(fp);

    fp = fopen("poem.txt", "r");
    if (fp == NULL) {
        return 1;
    }
    char line[256];
    while (fgets(line, sizeof(line), fp) != NULL) {
        printf("%s", line);
    }
    fclose(fp);
    return 0;
}
TEXT 📖 仅展示
白日依山尽
黄河入海流

4. 二进制文件读写

(1) fread / fwrite

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

参数含义:ptr 是数据缓冲区,size 是每个元素的字节数,count 是元素个数。返回值是实际读写的元素个数。

C
#include <stdio.h>

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

int main(void) {
    Student stu = {1, "张三", 92.5f};

    FILE *fp = fopen("student.dat", "wb");
    if (fp == NULL) {
        return 1;
    }
    fwrite(&stu, sizeof(Student), 1, fp);
    fclose(fp);

    Student read_stu;
    fp = fopen("student.dat", "rb");
    if (fp == NULL) {
        return 1;
    }
    fread(&read_stu, sizeof(Student), 1, fp);
    fclose(fp);

    printf("学号: %d, 姓名: %s, 成绩: %.1f\n",
           read_stu.id, read_stu.name, read_stu.score);
    return 0;
}
TEXT 📖 仅展示
学号: 1, 姓名: 张三, 成绩: 92.5
💡 二进制模式适合存储结构体数据,效率高且不会改变内容。文本模式在不同操作系统间可能换行符不同,二进制模式则原样保存。


5. 格式化读写

(1) fprintf / fscanf

C
int fprintf(FILE *fp, const char *format, ...);
int fscanf(FILE *fp, const char *format, ...);

用法和 printf/scanf 完全一样,只是多了一个文件指针参数。

C
#include <stdio.h>

int main(void) {
    FILE *fp = fopen("scores.txt", "w");
    if (fp == NULL) {
        return 1;
    }
    fprintf(fp, "%s %d %.1f\n", "李四", 2, 88.0);
    fprintf(fp, "%s %d %.1f\n", "王五", 3, 95.5);
    fclose(fp);

    fp = fopen("scores.txt", "r");
    if (fp == NULL) {
        return 1;
    }
    char name[20];
    int id;
    float score;
    while (fscanf(fp, "%s %d %f", name, &id, &score) == 3) {
        printf("姓名: %s, 学号: %d, 成绩: %.1f\n", name, id, score);
    }
    fclose(fp);
    return 0;
}
TEXT 📖 仅展示
姓名: 李四, 学号: 2, 成绩: 88.0
姓名: 王五, 学号: 3, 成绩: 95.5

6. 文件位置指针

每个打开的文件都有一个"位置指针",指示当前读写位置。就像读书时的书签——你读到哪,书签就夹在哪。

(1) fseek

C
int fseek(FILE *fp, long offset, int origin);
origin 含义
SEEK_SET 文件开头
SEEK_CUR 当前位置
SEEK_END 文件末尾

(2) ftell / rewind

C
long ftell(FILE *fp);
void rewind(FILE *fp);

ftell 返回当前位置(相对文件开头的字节数)。rewind 等价于 fseek(fp, 0, SEEK_SET)

▶ 示例

fseekftell 计算文件大小:

C
#include <stdio.h>

int main(void) {
    FILE *fp = fopen("data.txt", "rb");
    if (fp == NULL) {
        printf("文件打开失败\n");
        return 1;
    }
    fseek(fp, 0, SEEK_END);
    long size = ftell(fp);
    rewind(fp);
    printf("文件大小: %ld 字节\n", size);
    fclose(fp);
    return 0;
}
▶ 试一试

▶ 示例

从二进制文件中随机读取指定记录:

C
#include <stdio.h>

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

int main(void) {
    Student students[3] = {
        {1, "张三", 92.5f},
        {2, "李四", 88.0f},
        {3, "王五", 95.5f}
    };

    FILE *fp = fopen("students.dat", "wb");
    if (fp == NULL) {
        return 1;
    }
    fwrite(students, sizeof(Student), 3, fp);
    fclose(fp);

    fp = fopen("students.dat", "rb");
    if (fp == NULL) {
        return 1;
    }
    int target = 2;
    fseek(fp, (target - 1) * sizeof(Student), SEEK_SET);
    Student s;
    fread(&s, sizeof(Student), 1, fp);
    fclose(fp);

    printf("第%d条记录: 学号=%d, 姓名=%s, 成绩=%.1f\n",
           target, s.id, s.name, s.score);
    return 0;
}
▶ 试一试
TEXT 📖 仅展示
第2条记录: 学号=2, 姓名=李四, 成绩=88.0
🔥 这就是数据库"随机访问"的原理!通过计算偏移量直接跳到指定位置,不需要从头遍历。


7. 其他文件函数

(1) feof

C
int feof(FILE *fp);

检测是否到达文件末尾。到达返回非零值。注意:feof 是在读取操作失败后才变为真的,不是提前判断。

(2) fflush

C
int fflush(FILE *fp);

强制刷新缓冲区,把缓冲区数据写入文件。对输入流调用 fflush 的行为是未定义的。

(3) remove / rename

C
int remove(const char *filename);
int rename(const char *old, const char *new);

删除文件和重命名文件。成功返回 0


8. 文件操作的通用模式

大部分文件操作都遵循这个模式:

  1. fopen 打开文件,检查返回值
  2. 循环读写数据
  3. fclose 关闭文件
C
#include <stdio.h>

int main(void) {
    FILE *fp = fopen("input.txt", "r");
    if (fp == NULL) {
        perror("fopen");
        return 1;
    }

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

    fclose(fp);
    return 0;
}
⚠️ 检查 fopen 返回值是铁律!文件可能不存在、没有权限、磁盘满了……不检查就使用空指针会直接崩溃。

❓ 常见问题

Q fopen 返回 NULL 但文件明明存在,怎么回事?
A 可能是路径问题。相对路径基于程序运行目录,不是源文件目录。在 IDE 中运行时,工作目录可能和预期不同。
Q 文本模式和二进制模式有什么区别?
A 在 Windows 上,文本模式读写时会把 \n\r\n 互相转换,二进制模式不做任何转换。Linux 上两者没区别。
Q fwrite 写入结构体时,不同编译器生成的文件兼容吗?
A 不一定。内存对齐方式、字节序、基本类型大小都可能不同。跨平台需要手动序列化。
Q feof 为什么不能用来判断循环结束?
A feof 只有在读取操作越过文件末尾后才变为真。如果用它做循环条件,最后一次循环会多处理一条无效数据。应该用 fread/fgets 的返回值来判断。

📖 小节

📝 作业

  1. 编写程序,统计一个文本文件中的行数、单词数和字符数
  2. 实现一个简单的文件复制程序,用二进制模式读取源文件并写入目标文件
  3. 编写程序,将一个结构体数组写入二进制文件,再从文件中读取并打印出来
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏