【C语言程序设计】第35篇:文件的打开、关闭与读写操作

1 引言

文件操作的核心就是三步:打开 → 读写 → 关闭。其中,读写操作可以根据需求选择不同粒度的函数:

c

复制代码
#include <stdio.h>

int main(void)
{
    FILE *fp = fopen("data.txt", "w");
    if (fp == NULL) return 1;
    
    /* 字符写入 */
    fputc('A', fp);
    
    /* 字符串写入 */
    fputs("Hello\n", fp);
    
    /* 格式化写入 */
    fprintf(fp, "数字:%d,浮点数:%.2f\n", 100, 3.14);
    
    fclose(fp);
    return 0;
}

本章我们将详细学习这些函数的用法和适用场景。


2 文件的打开:fopen

2.1 函数原型

c

复制代码
#include <stdio.h>

FILE *fopen(const char *filename, const char *mode);
  • 功能:打开指定文件,返回文件指针

  • 参数filename 文件名,mode 打开模式

  • 返回值 :成功返回 FILE*,失败返回 NULL

2.2 打开模式详解

2.2.1 文本模式
模式 含义 文件存在 文件不存在
"r" 只读 打开成功 返回NULL
"w" 只写 清空内容 创建新文件
"a" 追加 写入末尾 创建新文件
"r+" 读写 打开成功 返回NULL
"w+" 读写 清空内容 创建新文件
"a+" 读+追加 写入末尾 创建新文件
2.2.2 二进制模式

在文本模式后加 b 表示二进制模式:

c

复制代码
"rb"   /* 二进制只读 */
"wb"   /* 二进制只写(清空) */
"ab"   /* 二进制追加 */
"rb+"  /* 二进制读写 */
"wb+"  /* 二进制读写(清空) */
"ab+"  /* 二进制读+追加 */
2.2.3 模式选择建议

c

复制代码
/* 读取配置文件 */
FILE *fp = fopen("config.ini", "r");

/* 写入日志(追加) */
FILE *fp = fopen("log.txt", "a");

/* 创建新文件(覆盖旧文件) */
FILE *fp = fopen("output.txt", "w");

/* 读取二进制文件(如图片) */
FILE *fp = fopen("image.jpg", "rb");

/* 需要同时读写同一个文件 */
FILE *fp = fopen("data.bin", "rb+");

2.3 打开失败的处理

c

复制代码
FILE *fp = fopen("data.txt", "r");
if (fp == NULL) {
    perror("打开文件失败");  /* 输出具体错误原因 */
    /* 或 */
    printf("无法打开文件,请检查文件是否存在\n");
    return 1;
}

perror 会根据全局变量 errno 输出错误描述,如 No such file or directory


3 文件的关闭:fclose

3.1 函数原型

c

复制代码
int fclose(FILE *stream);
  • 功能:关闭文件,刷新缓冲区

  • 返回值 :成功返回0,失败返回 EOF

3.2 为什么要关闭文件

c

复制代码
#include <stdio.h>

int main(void)
{
    FILE *fp = fopen("data.txt", "w");
    if (fp == NULL) return 1;
    
    fprintf(fp, "重要数据");  /* 数据可能在缓冲区,未写入磁盘 */
    
    fclose(fp);  /* 刷新缓冲区,确保数据写入磁盘 */
    
    return 0;
}

关闭文件的三个原因

  1. 刷新缓冲区:确保所有数据真正写入磁盘

  2. 释放资源:释放系统文件句柄(操作系统限制同时打开的文件数)

  3. 防止数据损坏:程序异常结束可能丢失缓冲区的数据

3.3 忘记关闭的后果

c

复制代码
void process_files(void)
{
    for (int i = 0; i < 10000; i++) {
        FILE *fp = fopen("temp.txt", "r");
        /* 处理文件... */
        /* 忘记 fclose(fp) */
    }
}  /* 可能耗尽系统文件句柄,后续无法打开文件 */

3.4 关闭后指针置空

c

复制代码
fclose(fp);
fp = NULL;  /* 防止悬空指针 */

4 字符读写函数

4.1 fgetc:读取一个字符

c

复制代码
int fgetc(FILE *stream);
  • 功能:从文件中读取一个字符

  • 返回值 :成功返回读取的字符(转为 int);失败或文件结束返回 EOF

c

复制代码
#include <stdio.h>

int main(void)
{
    FILE *fp = fopen("data.txt", "r");
    if (fp == NULL) return 1;
    
    int ch;
    while ((ch = fgetc(fp)) != EOF) {
        putchar(ch);  /* 输出到屏幕 */
    }
    
    fclose(fp);
    return 0;
}

注意 :返回类型是 int 而不是 char,因为 EOF(通常是-1)无法用 char 表示。

4.2 fputc:写入一个字符

c

复制代码
int fputc(int c, FILE *stream);
  • 功能:将一个字符写入文件

  • 返回值 :成功返回写入的字符,失败返回 EOF

c

复制代码
#include <stdio.h>

int main(void)
{
    FILE *fp = fopen("output.txt", "w");
    if (fp == NULL) return 1;
    
    for (char c = 'A'; c <= 'Z'; c++) {
        fputc(c, fp);
    }
    fputc('\n', fp);
    
    fclose(fp);
    return 0;
}

4.3 应用:文件复制(字符级)

c

复制代码
#include <stdio.h>

int copy_file(const char *src, const char *dst)
{
    FILE *fsrc = fopen(src, "r");
    if (fsrc == NULL) return -1;
    
    FILE *fdst = fopen(dst, "w");
    if (fdst == NULL) {
        fclose(fsrc);
        return -1;
    }
    
    int ch;
    while ((ch = fgetc(fsrc)) != EOF) {
        fputc(ch, fdst);
    }
    
    fclose(fsrc);
    fclose(fdst);
    return 0;
}

int main(void)
{
    if (copy_file("source.txt", "target.txt") == 0) {
        printf("复制成功\n");
    } else {
        printf("复制失败\n");
    }
    return 0;
}

5 字符串读写函数

5.1 fgets:读取一行字符串

c

复制代码
char *fgets(char *str, int n, FILE *stream);
  • 功能 :从文件读取一行(最多 n-1 个字符),遇到换行符或文件结束停止

  • 特点:会保留换行符(如果读取到的话)

  • 返回值 :成功返回 str,失败或文件结束返回 NULL

c

复制代码
#include <stdio.h>

int main(void)
{
    FILE *fp = fopen("data.txt", "r");
    if (fp == NULL) return 1;
    
    char line[256];
    while (fgets(line, sizeof(line), fp) != NULL) {
        printf("读取到:%s", line);  /* line 可能包含换行符 */
    }
    
    fclose(fp);
    return 0;
}

去除换行符

c

复制代码
char *p = line;
while (*p) {
    if (*p == '\n') {
        *p = '\0';
        break;
    }
    p++;
}

5.2 fputs:写入字符串

c

复制代码
int fputs(const char *str, FILE *stream);
  • 功能:将字符串写入文件

  • 特点:不会自动添加换行符

  • 返回值 :成功返回非负数,失败返回 EOF

c

复制代码
#include <stdio.h>

int main(void)
{
    FILE *fp = fopen("output.txt", "w");
    if (fp == NULL) return 1;
    
    fputs("第一行\n", fp);
    fputs("第二行\n", fp);
    fputs("第三行", fp);    /* 没有换行符 */
    
    fclose(fp);
    return 0;
}

5.3 应用:读取配置文件

c

复制代码
#include <stdio.h>
#include <string.h>

#define MAX_LINE 256

void trim_newline(char *str)
{
    size_t len = strlen(str);
    if (len > 0 && str[len-1] == '\n') {
        str[len-1] = '\0';
    }
}

int main(void)
{
    FILE *fp = fopen("config.txt", "r");
    if (fp == NULL) {
        printf("配置文件不存在,使用默认配置\n");
        return 0;
    }
    
    char line[MAX_LINE];
    while (fgets(line, sizeof(line), fp) != NULL) {
        trim_newline(line);
        
        /* 跳过空行和注释 */
        if (line[0] == '\0' || line[0] == '#') {
            continue;
        }
        
        printf("配置项:%s\n", line);
    }
    
    fclose(fp);
    return 0;
}

6 格式化读写函数

6.1 fprintf:格式化写入

c

复制代码
int fprintf(FILE *stream, const char *format, ...);

c

复制代码
#include <stdio.h>

int main(void)
{
    FILE *fp = fopen("students.txt", "r");
    if (fp == NULL) return 1;
    
    char name[50];
    int id;
    float score;
    int count = 0;
    
    while (fscanf(fp, "%s %d %f", name, &id, &score) == 3) {
        printf("读取到:%s %d %.1f\n", name, id, score);
        count++;
    }
    
    printf("共读取 %d 条记录\n", count);
    fclose(fp);
    return 0;
}

6.3 应用:学生成绩管理

c

复制代码
#include <stdio.h>
#include <string.h>

#define MAX_STUDENTS 100

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

/* 保存数据到文件 */
int save_students(Student students[], int n, const char *filename)
{
    FILE *fp = fopen(filename, "w");
    if (fp == NULL) return -1;
    
    for (int i = 0; i < n; i++) {
        fprintf(fp, "%s %d %.1f\n", 
                students[i].name, 
                students[i].id, 
                students[i].score);
    }
    
    fclose(fp);
    return 0;
}

/* 从文件读取数据 */
int load_students(Student students[], int *n, const char *filename)
{
    FILE *fp = fopen(filename, "r");
    if (fp == NULL) return -1;
    
    int count = 0;
    while (count < MAX_STUDENTS && 
           fscanf(fp, "%s %d %f", 
                  students[count].name, 
                  &students[count].id, 
                  &students[count].score) == 3) {
        count++;
    }
    
    *n = count;
    fclose(fp);
    return 0;
}

/* 打印学生列表 */
void print_students(Student students[], int n)
{
    printf("\n学生列表:\n");
    printf("姓名\t学号\t成绩\n");
    printf("----------------\n");
    for (int i = 0; i < n; i++) {
        printf("%s\t%d\t%.1f\n", 
               students[i].name, 
               students[i].id, 
               students[i].score);
    }
}

int main(void)
{
    Student students[MAX_STUDENTS];
    int n = 0;
    
    /* 加载已有数据 */
    if (load_students(students, &n, "students.txt") == 0) {
        printf("已加载 %d 条记录\n", n);
        print_students(students, n);
    }
    
    /* 添加新学生 */
    Student new_stu = {"赵六", 1004, 95.0};
    students[n++] = new_stu;
    
    /* 保存数据 */
    if (save_students(students, n, "students.txt") == 0) {
        printf("保存成功\n");
    }
    
    return 0;
}

7 三种读写方式的对比

函数 粒度 适用场景 特点
fgetc/fputc 字符 逐字符处理、文本分析 最灵活,效率最低
fgets/fputs 字符串 逐行处理配置文件、文本文件 保留换行符,安全(可限制长度)
fprintf/fscanf 格式化 结构化数据的读写 方便,但效率较低,可能有格式问题

c

复制代码
/* 字符级:逐字符复制 */
int ch;
while ((ch = fgetc(src)) != EOF) {
    fputc(ch, dst);
}

/* 字符串级:逐行处理 */
char line[256];
while (fgets(line, sizeof(line), fp) != NULL) {
    /* 处理每一行 */
}

/* 格式化读写:结构化数据 */
fprintf(fp, "%s %d %.2f\n", name, id, score);
fscanf(fp, "%s %d %f", name, &id, &score);

8 常见错误与注意事项

8.1 忘记检查文件指针

c

复制代码
FILE *fp = fopen("data.txt", "r");
fscanf(fp, "%d", &n);  /* fp 可能为 NULL! */

8.2 混淆 r+ 和 w+

c

复制代码
/* r+:不会清空文件,从开头读写 */
FILE *fp = fopen("data.txt", "r+");

/* w+:会清空文件! */
FILE *fp = fopen("data.txt", "w+");  /* 原有数据丢失! */

8.3 fgets 缓冲区大小不足

c

复制代码
char buf[10];
fgets(buf, 10, fp);  /* 最多读取9个字符 + '\0' */
/* 如果一行超过9个字符,只读取前9个,下次读取会继续 */

8.4 fscanf 格式不匹配

c

复制代码
/* 如果文件是 "abc 100" */
fscanf(fp, "%d", &n);  /* 失败!%d 无法匹配 'a' */
/* 返回0,n的值不变 */

8.5 忘记关闭文件

c

复制代码
FILE *fp = fopen("log.txt", "a");
fprintf(fp, "日志信息\n");
/* 忘记 fclose,数据可能还在缓冲区 */

8.6 对已关闭的文件操作

c

复制代码
FILE *fp = fopen("data.txt", "r");
fclose(fp);
fgetc(fp);  /* 错误!使用已关闭的文件指针 */

8.7 忽略 fscanf 返回值

c

复制代码
fscanf(fp, "%d", &n);  /* 没有检查返回值 */
if (fscanf(fp, "%d", &n) != 1) {
    printf("读取失败\n");
}

9 本章小结

本章系统介绍了文件的打开、关闭和三种读写方式:

1. 文件打开:fopen

2. 文件关闭:fclose

3. 字符级读写

4. 字符串级读写

5. 格式化读写

6. 选择建议

  • 功能:按格式将数据写入文件

  • 用法 :与 printf 完全类似,只是多了一个文件指针参数

  • 返回值:成功返回写入的字符数,失败返回负数

  • c

    复制代码
    #include <stdio.h>
    
    typedef struct {
        char name[50];
        int id;
        float score;
    } Student;
    
    int main(void)
    {
        Student students[] = {
            {"张三", 1001, 88.5},
            {"李四", 1002, 92.0},
            {"王五", 1003, 78.5}
        };
        int n = 3;
        
        FILE *fp = fopen("students.txt", "w");
        if (fp == NULL) return 1;
        
        for (int i = 0; i < n; i++) {
            fprintf(fp, "%s %d %.1f\n", 
                    students[i].name, 
                    students[i].id, 
                    students[i].score);
        }
        
        fclose(fp);
        return 0;
    }

    6.2 fscanf:格式化读取

    c

    复制代码
    int fscanf(FILE *stream, const char *format, ...);
  • 功能:按格式从文件中读取数据

  • 用法 :与 scanf 完全类似

  • 返回值 :成功匹配并赋值的个数,文件结束返回 EOF

  • 多种模式:"r""w""a" 及读写组合

  • 二进制模式加 b

  • 总是检查返回值是否为 NULL

  • 必须调用,确保缓冲区刷新

  • 释放系统资源

  • 关闭后指针置空

  • fgetc:读取一个字符,返回 int

  • fputc:写入一个字符

  • 适合逐字符处理

  • fgets:读取一行(安全,有长度限制),保留换行符

  • fputs:写入字符串,不自动添加换行符

  • 适合逐行处理文本文件

  • fprintf:格式化写入,类似 printf

  • fscanf:格式化读取,类似 scanf

  • 适合结构化数据读写

  • 注意检查返回值

  • 简单复制 → fgetc/fputc

  • 文本逐行处理 → fgets/fputs

  • 结构化数据 → fprintf/fscanf

相关推荐
Wect1 小时前
React Hooks 核心原理
前端·算法·typescript
kishu_iOS&AI1 小时前
Python - 链表浅析
开发语言·python·链表
美式请加冰2 小时前
字符串的介绍和使用
算法
不想写代码的星星2 小时前
告别 C 风格枚举:为什么你应该使用 enum class
c++
m0_733612212 小时前
C++20概念(Concepts)入门指南
开发语言·c++·算法
AI_搬运工2 小时前
从智能指针窥见现代C++的生存法则:告别内存泄漏,这篇就够了
c++
仰泳的熊猫2 小时前
题目2571:蓝桥杯2020年第十一届省赛真题-回文日期
数据结构·c++·算法·蓝桥杯
我喜欢就喜欢2 小时前
基于离散余弦变换的感知哈希算法:原理、实现与工程实践
算法·哈希算法
CODE_RabbitV2 小时前
【3min 解决】keil5 编译stm32 出现一堆 core_cm3.c 报错问题
c语言·stm32·嵌入式硬件