🛡️

身份验证

答对题目即可进入,验证你是谁

剩余尝试次数:3

文件操作基础

C语言学习教程

文件操作基础 — 让数据”活”到程序之外

到现在为止,我们写的所有程序都有一个共同的问题:数据存在变量里,运行完就没了。你辛辛苦苦输入 40 个学生的成绩,程序一关,全泡汤。

文件操作就是解决这个问题的。把数据写到磁盘文件中,下次打开程序再读出来——就像你把作业保存为 Word 文档,关了电脑明天还能接着写。

核心概念:FILE 指针

C 语言用一个叫 FILE 的结构体(定义在 <stdio.h>)来代表一个”打开的文件”。你不需要知道 FILE 里面有什么——只需要拿着它的指针,传给各种文件操作函数就行。

FILE *fp;  // fp 是一个"文件句柄",通过它操作文件

可以把 FILE * 想象成一根”遥控天线”——你用这根天线(指针)对文件发出各种指令(读、写、关闭)。

fopen — 打开文件

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

打开模式完整对照表

模式含义文件不存在文件已存在读写位置
"r"只读返回 NULL (失败)从头读文件开头
"w"只写创建新文件清空内容再写文件开头
"a"追加写创建新文件在末尾接着写文件末尾
"r+"读写返回 NULL (失败)可读可写文件开头
"w+"读写创建新文件清空内容后可读可写文件开头
"a+"读+追加写创建新文件可从任意位置读,写只能在末尾读位置:开头;写位置:末尾

二进制模式(后面会展开)

在模式字符串末尾加 b

模式含义
"rb"二进制只读
"wb"二进制只写
"ab"二进制追加
"r+b" / "rb+"二进制读写

在 Linux/Mac 上,"r""rb" 没有区别。但在 Windows 上:文本模式会做 \n\r\n 转换,二进制模式不会。如果你不确定,处理非文本文件一律用 b

新手最容易踩的坑

// 坑 1:不检查 fopen 的返回值
FILE *fp = fopen("nonexistent.txt", "r");
fprintf(fp, "hello");  // 文件没打开成功,fp 是 NULL,程序崩溃!

// 正确做法:
FILE *fp = fopen("data.txt", "r");
if (fp == NULL) {
    printf("文件打开失败!可能文件不存在或没有权限。\n");
    return 1;
}
// 坑 2:用 "w" 打开已有文件
FILE *fp = fopen("我的日记.txt", "w");  // 原来的日记全没了!
fprintf(fp, "新内容\n");
fclose(fp);
// 如果你只是想添加内容,应该用 "a"
// 坑 3:忘记 fclose
FILE *fp = fopen("data.txt", "w");
fprintf(fp, "hello");
// 程序结束前没调 fclose
// 可能导致:1. 数据没真正写入磁盘  2. 资源泄漏

fclose — 关闭文件

关闭文件做了三件事:

  1. 把缓冲区里没写完的数据真正写到磁盘(“刷新缓冲区”)
  2. 释放与文件相关的系统资源
  3. 使 FILE * 指针失效
fclose(fp);
fp = NULL;  // 好习惯:防止"悬空指针"

写文件

fprintf — 格式化写入(最常用)

printf 几乎一样,只是多了一个 FILE * 参数:

fprintf(fp, 格式字符串, 参数...);
#include <stdio.h>

int main() {
    FILE *fp = fopen("students.txt", "w");
    if (fp == NULL) {
        printf("无法创建文件!\n");
        return 1;
    }

    // 像写 printf 一样写 fprintf
    fprintf(fp, "===== 学生成绩表 =====\n");
    fprintf(fp, "姓名:%s\n", "张三");
    fprintf(fp, "学号:%d\n", 2024001);
    fprintf(fp, "语文:%.1f  数学:%.1f  英语:%.1f\n", 92.5, 88.0, 95.5);
    fprintf(fp, "平均分:%.1f\n", (92.5 + 88.0 + 95.5) / 3.0);
    fprintf(fp, "========================\n");

    fclose(fp);
    printf("文件写入完成!\n");
    return 0;
}

启动程序后,你会发现项目目录下多了一个 students.txt,打开就是:

===== 学生成绩表 =====
姓名:张三
学号:2024001
语文:92.5  数学:88.0  英语:95.5
平均分:92.0
========================

fputs — 写一个字符串(不自动换行)

fputs("这是一行文字\n", fp);   // 不自动加 \n,需要手动加
fputs("这是另一行\n", fp);

fputc — 写一个字符

fputc('A', fp);
fputc('\n', fp);

三种写函数的对比

函数用途自动换行?格式化?
fprintf格式化输出否(需 \n
fputs写字符串
fputc写单个字符

读文件

fgets — 逐行读取(最常用)

char *fgets(char *buffer, int size, FILE *stream);
#include <stdio.h>

int main() {
    FILE *fp = fopen("students.txt", "r");
    if (fp == NULL) {
        printf("文件打开失败!\n");
        return 1;
    }

    char line[256];   // 一行最多 255 个字符(余一个放 '\0')
    int line_number = 1;

    // fgets 返回 NULL 时表示文件读完了(或出错)
    while (fgets(line, sizeof(line), fp) != NULL) {
        printf("%d: %s", line_number, line);  // line 本身已含换行符
        line_number++;
    }

    fclose(fp);
    return 0;
}

注意fgets 读到换行符 \n 时会把它也存进 buffer。如果你不想要换行符,可以手动去掉:

// 去掉末尾的换行符
size_t len = strlen(line);
if (len > 0 && line[len - 1] == '\n') {
    line[len - 1] = '\0';
}

fscanf — 格式化读取

int fscanf(FILE *stream, const char *format, ...);

假设有一个文件 scores.txt 内容如下:

张三 92.5
李四 78.0
王五 85.0

读取代码:

#include <stdio.h>

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

    char name[32];
    float score;

    printf("===== 读到的成绩 =====\n");
    // fscanf 返回成功匹配并赋值的项数
    while (fscanf(fp, "%s %f", name, &score) == 2) {
        printf("姓名:%s,成绩:%.1f\n", name, score);
    }

    fclose(fp);
    return 0;
}

fscanf 的返回值非常重要:它表示成功读到了几个值。上例中,每次循环期望读到 2 个(一个字符串、一个浮点数),所以判断 == 2

fscanf 的局限:它遇到空格、换行就会分隔,所以不适合读取带空格的文本(如 "Zhang San" 会被当成两个字符串)。

fgetc — 读取一个字符

int ch;
while ((ch = fgetc(fp)) != EOF) {
    putchar(ch);  // 打印到屏幕
}

EOF(End Of File)是一个特殊值(通常是 -1),表示文件结束。

三种读函数的对比

函数用途适合场景
fgets读一行(含换行符)逐行处理文本,读带空格的内容
fscanf按格式读取文件格式规整,知道每行的结构
fgetc每次读一个字符逐个字符处理,需要精确控制

综合示例:一个简易日记程序

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

#define FILENAME "diary.txt"

// 写一篇新日记(追加模式)
void write_diary() {
    FILE *fp = fopen(FILENAME, "a");  // "a" = 追加模式
    if (fp == NULL) {
        printf("无法打开日记文件!\n");
        return;
    }

    // 获取当前时间
    time_t now = time(NULL);
    char *date_str = ctime(&now);
    // ctime 返回的字符串自带换行符,但我们想自定义格式
    date_str[strlen(date_str) - 1] = '\0';  // 去掉末尾的 \n

    fprintf(fp, "=== %s ===\n", date_str);

    printf("请输入今天的日记内容(输入 END 结束):\n");
    char line[512];
    while (1) {
        printf("> ");
        fgets(line, sizeof(line), stdin);

        // 去掉换行符
        line[strcspn(line, "\n")] = '\0';

        // 输入 END 结束
        if (strcmp(line, "END") == 0) break;

        fprintf(fp, "%s\n", line);
    }
    fprintf(fp, "\n");  // 日记之间空一行

    fclose(fp);
    printf("日记已保存。\n");
}

// 读所有日记
void read_diary() {
    FILE *fp = fopen(FILENAME, "r");
    if (fp == NULL) {
        printf("还没有日记呢,先去写一篇吧!\n");
        return;
    }

    printf("\n========== 我的日记 ==========\n");
    char line[512];
    while (fgets(line, sizeof(line), fp) != NULL) {
        printf("%s", line);  // line 自带 \n,所以不加 \n
    }
    printf("==============================\n");

    fclose(fp);
}

int main() {
    int choice;
    while (1) {
        printf("\n===== 简易日记本 =====\n");
        printf("1. 写日记\n");
        printf("2. 看日记\n");
        printf("3. 退出\n");
        printf("请选择(1-3):");
        scanf("%d", &choice);
        getchar();  // 吃掉 scanf 留下的换行符

        switch (choice) {
            case 1: write_diary();  break;
            case 2: read_diary();   break;
            case 3:
                printf("再见!\n");
                return 0;
            default:
                printf("无效选择,请重试。\n");
        }
    }
}

这个程序把之前学的循环、分支、字符串、函数、文件操作全串起来了。试着运行它,写几篇日记,关闭程序再打开——日记都在。

常用模式速查

模式 1:创建新文件并写入

FILE *fp = fopen("output.txt", "w");
if (fp == NULL) { /* 错误处理 */ return 1; }
fprintf(fp, "内容\n");
fclose(fp);

模式 2:读取已有文件所有内容

FILE *fp = fopen("input.txt", "r");
if (fp == NULL) { /* 错误处理 */ return 1; }
char line[256];
while (fgets(line, sizeof(line), fp) != NULL) {
    // 处理每一行
    printf("%s", line);
}
fclose(fp);

模式 3:追加内容到文件末尾

FILE *fp = fopen("log.txt", "a");
if (fp == NULL) { /* 错误处理 */ return 1; }
fprintf(fp, "新的日志条目\n");
fclose(fp);

模式 4:统计文件行数

FILE *fp = fopen("data.txt", "r");
if (fp == NULL) return 1;
int lines = 0;
char buf[256];
while (fgets(buf, sizeof(buf), fp) != NULL) {
    lines++;
}
printf("文件共 %d\n", lines);
fclose(fp);

模式 5:复制文件内容

FILE *src = fopen("source.txt", "r");
FILE *dst = fopen("dest.txt", "w");
if (src == NULL || dst == NULL) {
    printf("打开文件失败!\n");
    return 1;
}
char line[1024];
while (fgets(line, sizeof(line), src) != NULL) {
    fputs(line, dst);  // 逐行复制
}
fclose(src);
fclose(dst);
printf("复制完成!\n");

常见错误速查

症状常见原因
程序崩溃(segfault)fopen 失败返回 NULL,你用 NULL 指针调用了 fprintf
文件内容总是空的忘了 fclose,缓冲区没刷新
w 模式下旧内容消失这正是 w 的行为——先清空再写。用 a 追加
读取的内容多了奇怪字符fgets 的 buffer 不够大,一行被截断了
fscanf 死循环读到最后无法匹配格式,但也没到 EOF。用返回值判断
中文乱码Windows 控制台默认 GBK,检查文件编码是否为 UTF-8

要点速查

概念说明
FILE *文件指针(文件句柄),所有文件操作的核心
fopen(path, mode)打开文件,失败返回 NULL
fclose(fp)关闭文件,刷新缓冲区,释放资源
"r" / "w" / "a"读 / 写(清空)/ 追加
fprintf格式化写入,用法和 printf 一样
fgets(buf, size, fp)读一行(含换行符),文件尾返回 NULL
fscanf格式化读取,返回值 = 成功读取的项数
fputc / fgetc写一个字符 / 读一个字符
fputs写一个字符串(不自动换行)

掌握了文件操作,你的程序就真正有了”记忆”——数据可以跨越程序的启动和关闭而持久存在。下一课我们学习二进制文件读写和错误处理,让你能高效处理更复杂的数据。