课堂

C语言文件读写

C 语言文件操作笔记(File I/O)

适用:C 语言入门、CS50。头文件:#include <stdio.h>。

1. 基本概念

C 使用 文件流(stream) 读写文件,通过 FILE * 操作。FILE * 是指向标准库文件流对象的指针,不是指向磁盘内容的 char *。

标准流程:fopen() 打开 → 读写 → fclose() 关闭。

#include <stdio.h>

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

    if (fprintf(fp, "Hello, world!\n") < 0)
    {
        perror("fprintf");
        fclose(fp);
        return 1;
    }

    if (fclose(fp) == EOF)
    {
        perror("fclose");
        return 1;
    }
    return 0;
}

相对文件路径相对于程序运行时的当前工作目录,不一定是源代码所在目录。

2. fopen():打开文件

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

成功返回 FILE *;失败返回 NULL。

模式 读 写 文件不存在 已有内容
"r" ✓ — 失败 保留
"w" — ✓ 创建 清空
"a" — ✓(追加) 创建 保留
"r+" ✓ ✓ 失败 保留
"w+" ✓ ✓ 创建 清空
"a+" ✓ ✓(追加) 创建 保留
  • 后缀 b:二进制模式,例如 "rb"、"wb";在部分系统上影响换行转换。
  • +:允许读写;同一个更新流上交替读写时通常需要适当调用定位或刷新函数,不能直接任意切换。
  • "w" / "w+" 打开已有文件时立即截断文件,请谨慎。
FILE *fp = fopen("data.txt", "r");
if (fp == NULL)
{
    perror("Cannot open data.txt");
    return 1;
}

3. 文本文件写入

函数 用途 示例
fputc(c, fp) 写一个字符 fputc('A', fp);
fputs(s, fp) 写字符串 fputs("Hello\n", fp);
fprintf(fp, fmt, ...) 格式化写入 fprintf(fp, "%d\n", 42);
FILE *fp = fopen("scores.txt", "w");
if (fp == NULL) return 1;

fputc('A', fp);
fputs("lice ", fp);       // 不会自动补换行
fprintf(fp, "%d\n", 95);

if (fclose(fp) == EOF) return 1;

printf(...) 面向标准输出 stdout,而 fprintf(fp, ...) 面向指定流。

4. 文本文件读取

4.1 fgetc():逐字符读取

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

int c;
while ((c = fgetc(fp)) != EOF)
{
    putchar(c);
}
if (ferror(fp))
{
    perror("fgetc");
    fclose(fp);
    return 1;
}
fclose(fp);

为什么用 int c? fgetc() 要同时表示有效字符和特殊值 EOF,使用 char 可能无法可靠区分。

EOF 不是文件末尾真实存在的字符;它是读取函数用于表示文件结束或出错的返回值。

4.2 fgets():读取一行(或一部分)

char buffer[100];
while (fgets(buffer, sizeof(buffer), fp) != NULL)
{
    printf("%s", buffer);
}
  • 最多读取 sizeof(buffer) - 1 个字符,留下一个位置放 \0。
  • 如读到换行符且空间足够,会保留 \n。
  • 一行过长时,可能需要多次 fgets() 才能读完。

4.3 fscanf():格式化读取

假设文件内容:

Alice 90
Bob 85
char name[50];
int score;
while (fscanf(fp, "%49s %d", name, &score) == 2)
{
    printf("%s: %d\n", name, score);
}

fscanf() 返回成功赋值的字段个数;%49s 防止将超过容量的字符串写入 name[50]。%s 遇空白字符即停止,不能读取包含空格的完整姓名。

5. 文件位置:ftell、fseek、rewind

函数 作用
ftell(fp) 获取当前位置标记,失败为 -1L
fseek(fp, offset, origin) 修改当前位置,成功返回 0
rewind(fp) 回到文件开头,同时清除错误及 EOF 状态标志

fseek() 的参考位置:

  • SEEK_SET:文件开头
  • SEEK_CUR:当前位置
  • SEEK_END:文件末尾

二进制文件示例:

fseek(fp, 0, SEEK_SET);   // 开头
fseek(fp, 10, SEEK_SET);  // 从开头偏移 10 字节
fseek(fp, -4, SEEK_END);  // 从末尾向前偏移 4 字节(需确保位置合法)

文本流的偏移存在标准限制,不能普遍假定可按任意字节偏移;ftell() 在文本流中得到的值主要用作恢复位置的标记。

6. 二进制读写:fread() 与 fwrite()

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

返回值都是成功处理的完整元素个数,而不是字节数。

#include <stdio.h>

int main(void)
{
    int numbers[] = {10, 20, 30, 40};
    FILE *fp = fopen("numbers.bin", "wb");
    if (fp == NULL) return 1;

    size_t written = fwrite(numbers, sizeof(numbers[0]), 4, fp);
    if (written != 4)
    {
        perror("fwrite");
        fclose(fp);
        return 1;
    }
    if (fclose(fp) == EOF) return 1;

    int loaded[4];
    fp = fopen("numbers.bin", "rb");
    if (fp == NULL) return 1;

    size_t read_count = fread(loaded, sizeof(loaded[0]), 4, fp);
    if (read_count != 4)
    {
        fprintf(stderr, "Only read %zu elements\n", read_count);
        fclose(fp);
        return 1;
    }
    fclose(fp);
    return 0;
}

提示:直接将 int 的内存表示写进文件,受 int 大小及大小端(Endianness)影响,不保证跨平台通用。稳定的文件格式应明确字节布局。

7. EOF、feof()、ferror()

不要写:

while (!feof(fp))  // 不推荐:在读取尝试到达末尾后才设置 EOF 标志
{
    int c = fgetc(fp);
    // 可能误处理 EOF
}

正确方式:根据读取函数的返回值控制循环,再检查错误。

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

if (ferror(fp))
{
    perror("Read error");
}
else if (feof(fp))
{
    puts("Reached end of file");
}

8. 综合实例:复制任意二进制文件

#include <stdio.h>

int main(void)
{
    FILE *src = fopen("input.bin", "rb");
    if (src == NULL)
    {
        perror("input");
        return 1;
    }

    FILE *dst = fopen("output.bin", "wb");
    if (dst == NULL)
    {
        perror("output");
        fclose(src);
        return 1;
    }

    unsigned char buffer[4096];
    size_t n;
    int failed = 0;

    while ((n = fread(buffer, 1, sizeof(buffer), src)) > 0)
    {
        if (fwrite(buffer, 1, n, dst) != n)
        {
            perror("write");
            failed = 1;
            break;
        }
    }

    if (ferror(src))
    {
        perror("read");
        failed = 1;
    }
    if (fclose(src) == EOF)
    {
        perror("close input");
        failed = 1;
    }
    if (fclose(dst) == EOF)
    {
        perror("close output");
        failed = 1;
    }
    return failed;
}

确保源文件与目标文件不是同一个文件,否则 "wb" 打开目标文件时可能清空源数据。

9. 文件流与动态内存对比

比较 malloc() fopen()
返回 内存指针(如 int *) 文件流指针 FILE *
使用 *p、p[i] fgetc、fprintf、fread 等
释放 free(p) fclose(fp)
失败时 NULL NULL

不能使用 free(fp) 代替 fclose(fp);fclose() 之后也不能继续使用该文件流。

10. 速查表

函数 用途 关键注意事项
fopen 打开文件 检查 NULL
fclose 关闭文件 检查 EOF,写入错误也可能在关闭时暴露
fgetc 读字符 使用 int 接收
fputc 写字符 失败返回 EOF
fgets 读文本 有长度限制,可能保留换行
fputs 写字符串 不自动换行
fscanf 格式化读 检查成功赋值项数
fprintf 格式化写 返回负值表示出错
fread 二进制读 返回完整元素数
fwrite 二进制写 返回完整元素数
fseek 移动位置 文本流有限制
ftell 查询位置 出错返回 -1L
rewind 回到开头 清除 EOF 和错误标志
feof 测 EOF 状态 不作为读取循环的预先判断
ferror 检查流错误 和 feof 区分
perror 输出错误描述 用于错误诊断
remove 删除文件 成功返回 0
rename 重命名文件 成功返回 0

11. 常见错误与练习

常见错误: 忘记判断 fopen 结果;误用 "w" 导致数据被清空;读取 EOF 时使用 char;不检查读取结果;fclose 后继续操作流;忘记检查写入和关闭错误。

建议练习:

  1. 读取文本文件,统计字节数、单词数和行数。
  2. 将一个文本文件复制到另一个文本文件。
  3. 使用 fread / fwrite 复制一张图片并对比源、目标文件。
  4. 将学生姓名与分数保存为 CSV 文件,再读取并求平均值。

核心: FILE * 管理的是文件流;读写函数操作流;EOF 是读取返回状态,不是文件中的字符;成功打开的流应该正确关闭。