C 文件操作

本章导览 程序运行时数据存放在内存中,程序结束后数据就消失了。要想持久化保存数据,必须将数据写入文件。本章讲解 C 语言中文件操作的完整流程。 本章涵盖:文件基本知识 → 打开与关闭文件 → 顺序读写(字符、字符串、格式化、二进制) → 随机读写 → 出错检测 核心流程:定义文件指针 → fopen 打开 → 读写操作 → fclose 关闭 相关基础:c-io、c-pointers、c-custom-types

Abstract: 本章导览 程序运行时数据存放在内存中,程序结束后数据就消失了。要想持久化保存数据,必须将数据写入文件。本章讲解 C 语言中文件操作的完整流程。

本章涵盖:文件基本知识 → 打开与关闭文件 → 顺序读写(字符、字符串、格式化、二进制) → 随机读写 → 出错检测

核心流程定义文件指针fopen 打开读写操作fclose 关闭

相关基础:c-ioc-pointersc-custom-types


1 C 文件的有关基本知识

1.1 什么是文件

文件是存储在外部介质(磁盘等)上的数据的集合

在程序设计中,主要用到两种文件:

类型 说明
程序文件 源程序文件(.c)、目标文件(.obj)、可执行文件(.exe
数据文件 程序运行时读写的数据存储文件(本章重点

Note: 操作系统视角 操作系统把各种输入输出设备都统一作为文件处理。终端键盘是输入文件,显示屏和打印机是输出文件

流(Stream)的概念

输入输出是数据传送的过程,数据如流水一样从一处流向另一处,因此常将输入输出形象地称为(Stream)。

  • 输入操作:数据从文件流向计算机内存
  • 输出操作:数据从计算机流向文件

C 语言把文件看作是一个字符(或字节)的序列。一个输入输出流就是一个字符流或字节流。

流式文件

C 语言的文件是流式文件——数据文件由一连串的字符(或字节)组成,不考虑行的界限,两行数据间不会自动加分隔符。对文件的存取以字符(字节)为单位,输入输出数据流的开始和结束仅受程序控制而不受物理符号控制。


1.2 文件名

文件标识包括三部分:

文件路径 + 文件名主干 + 文件后缀

例如:D:\CC\temp\file1.dat

部分 示例 说明
文件路径 D:\CC\temp\ 文件在外部存储设备中的位置
文件名主干 file1 用户指定的名称
文件后缀 .dat 表示文件类型

1.3 文件的分类

根据数据的组织形式,数据文件可分为:

ASCII 文件(文本文件)

在外存上以 ASCII 代码形式存储,每一个字节存放一个字符的 ASCII 码。

二进制文件

把内存中二进制形式存储的数据不加转换地输出到外存。

Tip: 字符 vs 数值的存储

  • 字符一律以 ASCII 形式存储
  • 数值型数据既可以用 ASCII 形式存储,也可以用二进制形式存储

示例:整数 10000 的两种存储方式

存储方式 占用字节 内容
ASCII 形式 5 字节 '1' '0' '0' '0' '0'(每个字符占 1 字节)
二进制形式 4 字节(int) 00000000 00000000 00100111 00010000(直接存储二进制值)

Tip: 选择建议

  • 文本文件:可读性好,可以用文本编辑器查看,但占用空间大
  • 二进制文件:节省空间、读写速度快,但不能直接阅读

1.4 文件缓冲区

ANSI C 标准采用缓冲文件系统处理数据文件。

系统自动在内存中为程序中每一个正在使用的文件开辟一个文件缓冲区

输出过程(内存 → 磁盘)

程序数据区 ──→ 输出文件缓冲区 ──→ 磁盘
                (装满后一次性写入)

从内存向磁盘输出数据,必须先送到缓冲区,装满缓冲区后才一起送到磁盘。

输入过程(磁盘 → 内存)

磁盘 ──→ 输入文件缓冲区 ──→ 程序数据区
          (一次性读入充满)     (逐个取出)

从磁盘读入数据,一次从磁盘读一批数据充满缓冲区,然后再从缓冲区逐个取数据给程序变量。

Note: 为什么需要缓冲区 磁盘的读写速度远慢于 CPU,缓冲区可以减少磁盘 I/O 次数,提高效率。


1.5 文件类型指针

每个被使用的文件都在内存中开辟一个相应的文件信息区,用来存放文件的有关信息(文件名、文件状态、文件当前位置等)。

这些信息保存在一个结构体变量中,该结构体类型由系统声明,取名为 FILE

Important: FILE 类型 FILE 结构体类型的声明包含在头文件 stdio.h 中。我们一般不直接操作 FILE 结构体,而是通过指向 FILE 类型的指针来引用。

定义文件指针

FILE *fp1, *fp2, *fp3;

每个文件指针对应一个文件:

fp1 ──→ 文件 f1 的文件信息区
fp2 ──→ 文件 f2 的文件信息区
fp3 ──→ 文件 f3 的文件信息区

3 个标准流文件

系统预定义了 3 个标准的流文件,程序运行时自动打开

标准流 名称 含义
stdin 标准输入流 默认为键盘输入
stdout 标准输出流 默认为屏幕输出
stderr 标准出错输出流 默认为屏幕输出

Tip: 我们日常使用的 printfscanf printf 本质上是向 stdout 写数据,scanf 是从 stdin 读数据。文件读写函数(如 fprintffscanf)只是把目标从标准流换成了文件流。


2 打开与关闭文件

2.1 用 fopen 函数打开数据文件

对文件读写之前,**必须先“打开”**该文件;使用结束之后,**必须“关闭”**该文件。

fopen 函数

FILE *fopen(const char *文件名, const char *使用文件方式);

调用示例

FILE *fp;
fp = fopen("a1.dat", "r");

这条语句做了三件事:

  1. 告诉系统要访问的文件名为 "a1.dat"
  2. 指定使用方式为 "r"(只读)
  3. 让指针变量 fp 指向被打开的文件

Caution: 打开失败的处理 如果打开失败,fopen 返回空指针 NULL。必须检查返回值:

if ((fp = fopen("file1", "r")) <mark> NULL) {
    printf("cannot open this file\n");
    exit(0);
}

文件打开方式一览表

使用方式 含义 说明
"r" / "rt" 只读 打开一个文本文件,只允许数据
"w" / "wt" 只写 打开或新建一个文本文件,只允许数据(原有内容被清除)
"a" / "at" 追加 打开一个文本文件,在文件末尾追加数据
"rb" 只读 打开一个二进制文件,只允许读
"wb" 只写 打开或新建一个二进制文件,只允许写
"ab" 追加 打开一个二进制文件,在文件末尾追加
"r+" / "rt+" 读写 打开一个文本文件,允许读和写
"w+" / "wt+" 读写 打开或新建一个文本文件,允许读写(原有内容被清除)
"a+" / "at+" 读写追加 打开一个文本文件,允许读,或在文件末尾追加
"rb+" 读写 打开一个二进制文件,允许读和写
"wb+" 读写 打开或新建一个二进制文件,允许读和写
"ab+" 读写追加 打开一个二进制文件,允许读,或在文件末尾追加

Warning: "w" 模式会清空文件"w" 打开已有文件时,原有内容会被全部清除!如果只想在末尾添加数据,应使用 "a"(追加模式)。


2.2 用 fclose 函数关闭数据文件

int fclose(FILE *fp);

调用示例

fclose(fp);

Danger: 必须关闭文件! 如果不关闭文件,缓冲区中的数据可能丢失!关闭文件时,系统会把缓冲区中尚未写入磁盘的数据全部写入,然后释放文件信息区和缓冲区。

完整的文件操作框架

int main()
{
    FILE *fp;

    // 1. 打开文件(检查是否成功)
    if ((fp = fopen("file1", "r")) </mark> NULL) {
        printf("cannot open this file\n");
        exit(0);
    }

    // 2. 进行读写操作
    // ......

    // 3. 关闭文件
    fclose(fp);

    return 0;
}

3 顺序读写数据文件

顺序读写的特点:

  • 写入时,先写的数据在文件前面,后写的在后面
  • 读取时,先读前面的数据,后读后面的
  • 数据的读写顺序与在文件中的物理顺序一致

3.1 怎样向文件读写字符

fgetc 和 fputc 函数

函数 调用形式 功能 返回值
fgetc fgetc(fp) fp 指向的文件读入一个字符 成功:返回所读字符;失败/结束:返回 EOF(即 -1)
fputc fputc(ch, fp) 把字符 ch 写到 fp 指向的文件中 成功:返回输出的字符;失败:返回 EOF

feof 函数——检测文件是否结束

函数 调用形式 功能 返回值
feof feof(fp) 检查文件是否到达末尾 未结束:返回 0;已结束:返回非 0

例 2:复制文件内容

file1.dat 的内容复制到 file2.dat

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

int main()
{
    FILE *in, *out;
    char ch, infile[10], outfile[10];

    printf("输入读入文件的名字:");
    scanf("%s", infile);
    printf("输入输出文件的名字:");
    scanf("%s", outfile);

    if ((in = fopen(infile, "r")) <mark> NULL) {
        printf("无法打开此文件\n");
        exit(0);
    }
    if ((out = fopen(outfile, "w")) </mark> NULL) {
        printf("无法打开此文件\n");
        exit(0);
    }

    while (!feof(in)) {          // 当文件未结束时循环
        ch = fgetc(in);          // 从源文件读一个字符
        fputc(ch, out);          // 写到目标文件
        putchar(ch);             // 同时显示在屏幕上
    }

    putchar(10);                 // 输出换行符
    fclose(in);
    fclose(out);
    return 0;
}

Tip: 逐字符复制的模式

while (!feof(fp)) {
    ch = fgetc(fp);     // 读一个字符
    // 处理 ch ...
}

这是最基本的文件逐字符遍历模式,几乎所有字符级文件操作都以此为基础。


3.2 怎样向文件读写一个字符串

fgets 和 fputs 函数

函数 调用形式 功能 返回值
fgets fgets(str, n, fp) fp 读入长度为 (n-1) 的字符串,存入 str 成功:返回 str 地址;失败:返回 NULL
fputs fputs(str, fp) str 指向的字符串写到 fp 指向的文件 成功:返回 0;失败:返回 EOF

fgets 详细说明

fgets(str, n, fp);
  • 参数 n 是要求得到的字符个数,但实际只读 n-1 个字符,然后在末尾自动加 '\0',共 n 个字符存入 str
  • 如果在读完 n-1 个字符之前遇到 换行符 '\n'文件结束符 EOF,读入即结束
  • 遇到的换行符 '\n' 也会作为一个字符被读入

fputs 详细说明

fputs("China", fp);
  • 第一个参数可以是字符串常量、字符数组名或字符型指针
  • 字符串末尾的 '\0' 不输出

例 3:字符串排序后写入文件

从键盘读入若干字符串,按字母顺序排序后保存到磁盘文件。

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

int main()
{
    FILE *fp;
    char str[3][10], temp[10];
    int i, j, k, n = 3;

    // 输入字符串
    printf("Enter strings:\n");
    for (i = 0; i < n; i++)
        gets(str[i]);

    // 选择排序(按字母顺序)
    for (i = 0; i < n - 1; i++) {
        k = i;
        for (j = i + 1; j < n; j++)
            if (strcmp(str[k], str[j]) > 0)
                k = j;
        if (k != i) {
            strcpy(temp, str[i]);
            strcpy(str[i], str[k]);
            strcpy(str[k], temp);
        }
    }

    // 写入文件
    if ((fp = fopen("D:\\CC\\string.dat", "w")) <mark> NULL) {
        printf("can't open file!\n");
        exit(0);
    }

    printf("\nThe new sequence:\n");
    for (i = 0; i < n; i++) {
        fputs(str[i], fp);       // 写入字符串
        fputs("\n", fp);         // 写入换行符
        printf("%s\n", str[i]);  // 屏幕显示
    }

    fclose(fp);
    return 0;
}

3.3 用格式化的方式读写文件

fprintf 和 fscanf 函数

这两个函数与 printfscanf 用法几乎相同,只是多了一个文件指针参数。

fprintf(文件指针, 格式字符串, 输出表列);
fscanf(文件指针, 格式字符串, 输入表列);

对比

标准 I/O 文件 I/O 区别
printf("格式", 变量...) fprintf(fp, "格式", 变量...) 输出目标从屏幕变为文件
scanf("格式", &变量...) fscanf(fp, "格式", &变量...) 输入来源从键盘变为文件

示例

// 将整数 i 和浮点数 f 写入文件
fprintf(fp, "%d,%6.2f", i, f);

// 从文件读取整数和浮点数
fscanf(fp, "%d,%f", &i, &f);

Tip: fprintf / fscanf 的本质 其实 printf(...) 等价于 fprintf(stdout, ...)scanf(...) 等价于 fscanf(stdin, ...)。文件 I/O 函数只是把标准流替换成了文件流。

综合应用示例:学生成绩处理

从文件读入 10 个学生 5 门课程的成绩数据,进行统计分析:

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

#define M 10
#define N 5

float Score[M][N] = {0}, Average1[M] = {0}, Average2[N] = {0}, Average;
int Max_row, Max_col;

// 从文件输入数据
void input()
{
    int i, j;
    float tmp;
    FILE *fp;
    if ((fp = fopen("xt7-14.txt", "r")) </mark> NULL) {
        printf("error");
        exit(0);
    }
    for (i = 0; i < M; i++)
        for (j = 0; j < N; j++) {
            fscanf(fp, "%f", &tmp);    // 用 fscanf 从文件读取
            Score[i][j] = tmp;
        }
    fclose(fp);
}

// 计算各项平均分
void average()
{
    int i, j;
    for (i = 0; i < M; i++)
        for (j = 0; j < N; j++) {
            Average1[i] += Score[i][j] / N;   // 每个学生的平均分
            Average2[j] += Score[i][j] / M;   // 每门课程的平均分
            Average += Score[i][j] / M / N;   // 总平均分
        }
}

// 找最高分
void findhigh()
{
    int i, j;
    Max_row = 0;
    Max_col = 0;
    for (i = 0; i < M; i++)
        for (j = 0; j < N; j++)
            if (Score[i][j] > Score[Max_row][Max_col]) {
                Max_row = i;
                Max_col = j;
            }
}

3.4 用二进制方式向文件读写一组数据

fread 和 fwrite 函数

用于以二进制方式一次读写一块数据,适合读写结构体、数组等整块数据。

size_t fread(void *buffer, size_t size, size_t count, FILE *fp);
size_t fwrite(const void *buffer, size_t size, size_t count, FILE *fp);
参数 含义
buffer 主存中数据的地址(读入到哪 / 从哪写出)
size 每个数据项的字节数
count 要读写多少个数据项
fp 文件指针

示例

struct Student {
    long num;
    char name[20];
    float score;
};

struct Student stu[10];

// 将 10 个学生的数据一次性写入二进制文件
fwrite(stu, sizeof(struct Student), 10, fp);

// 从二进制文件一次性读入 10 个学生的数据
fread(stu, sizeof(struct Student), 10, fp);

// 也可以一次只读一个
fread(&stu[0], sizeof(struct Student), 1, fp);

Tip: fread/fwrite vs fprintf/fscanf

特性 fread/fwrite fprintf/fscanf
文件类型 二进制文件 文本文件
速度 快(无格式转换) 慢(需要格式转换)
可读性 不可直接阅读 可用文本编辑器查看
精度 无损(二进制精确复制) 可能有精度损失
适用场景 结构体、大量数据 需要人工查看的数据

4 随机读写数据文件

4.1 文件位置标记及其定位

文件位置标记

文件被打开后,系统维护一个文件位置标记(文件位置指针),指向当前要读写的位置。

文件头 ────────── 读写当前位置 ────────── 文件尾

                  文件位置标记

顺序读写时,位置标记自动后移。随机读写则是手动改变位置标记的位置。

1. rewind 函数——回到文件开头

void rewind(FILE *fp);

使文件位置标记重新返回文件开头,没有返回值。

2. fseek 函数——移动到任意位置

int fseek(FILE *fp, long 位移量, int 起始点);
起始点 符号常量 数字代号
文件开始位置 SEEK_SET 0
文件当前位置 SEEK_CUR 1
文件末尾位置 SEEK_END 2

位移量:以起始点为基点,向前(正值)或向后(负值)移动的字节数。位移量应是 long 型(数字末尾加 L)。

fseek 调用示例

fseek(fp, 100L, 0);    // 从文件开头向前移动 100 字节
fseek(fp, 50L, 1);     // 从当前位置向前移动 50 字节
fseek(fp, -10L, 2);    // 从文件末尾向回退 10 字节

Note: fseek 一般用于二进制文件**** 文本文件中,由于字符编码和换行符转换等因素,fseek 的位移计算可能不准确。建议在二进制文件中使用。

3. ftell 函数——获取当前位置

long ftell(FILE *fp);

返回文件位置标记的当前位置(相对于文件开头的字节偏移量)。出错时返回 -1L

long i;
i = ftell(fp);
if (i <mark> -1L)
    printf("error\n");

4.2 随机读写

随机读写的核心:先用 fseek 定位到目标位置,再用 fread/fwrite 等函数读写。

例 6:读取第 1、3、5、7、9 个学生的数据

磁盘文件上存有 10 个学生数据(二进制格式),要求读取奇数位学生的数据并显示:

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

struct Student {
    long num;
    char name[20];
    float score;
};

int main()
{
    FILE *fp;
    struct Student stu;
    int i;

    if ((fp = fopen("students.dat", "rb")) </mark> NULL) {
        printf("cannot open file\n");
        exit(0);
    }

    // 读取第 1, 3, 5, 7, 9 个学生(下标 0, 2, 4, 6, 8)
    for (i = 0; i < 10; i += 2) {
        fseek(fp, (long)i * sizeof(struct Student), SEEK_SET);
        fread(&stu, sizeof(struct Student), 1, fp);
        printf("%-10ld %-20s %6.1f\n", stu.num, stu.name, stu.score);
    }

    fclose(fp);
    return 0;
}

Tip: 随机读写的关键 fseek(fp, (long)i * sizeof(struct Student), SEEK_SET);

i 个记录的起始位置 = i × 每条记录的字节数。这就是为什么二进制文件中所有记录大小固定时,随机访问非常方便。


5 文件读写的出错检测

ferror 函数

int ferror(FILE *fp);
返回值 含义
0 未出错
非 0 出错

Caution: 使用注意

  • 每次调用输入输出函数都会产生新的 ferror 值,因此应在调用 I/O 函数后立即检查
  • 调用 fopen 时,ferror 的初始值自动置为 0
if (ferror(fp)) {
    printf("文件读写出错!\n");
}

clearerr 函数

void clearerr(FILE *fp);

作用是将文件的错误标志文件结束标志都置为 0

  • 当调用 I/O 函数出错(ferror 为非零)时,立即调用 clearerr(fp) 清除错误标志,以便进行下一次检测
  • 错误标志一旦被设置就会一直保留,直到调用 clearerrrewind 或其他 I/O 函数时才被重置
// 出错处理模式
if (ferror(fp)) {
    printf("Error occurred!\n");
    clearerr(fp);        // 清除错误标志
}

本章总结

文件操作的完整流程

// 1. 定义文件指针
FILE *fp;

// 2. 打开文件
fp = fopen("文件名", "打开方式");
if (fp == NULL) { /* 错误处理 */ exit(0); }

// 3. 读写操作
// fgetc / fputc    —— 字符级
// fgets / fputs    —— 字符串级
// fscanf / fprintf —— 格式化(文本)
// fread / fwrite   —— 二进制块

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

核心函数速查表

函数 功能 适用场景
fopen 打开文件 所有文件操作前
fclose 关闭文件 所有文件操作后
feof 检测是否到文件末尾 循环读取文件
fgetc 读一个字符 字符级读取
fputc 写一个字符 字符级写入
fgets 读一个字符串 逐行读取文本
fputs 写一个字符串 逐行写入文本
fscanf 格式化读取 读取格式化文本数据
fprintf 格式化写入 写入格式化文本数据
fread 二进制读取 读结构体/大块数据
fwrite 二进制写入 写结构体/大块数据
fseek 移动文件位置标记 随机访问
rewind 回到文件开头 重新遍历文件
ftell 获取当前位置 记录/恢复文件位置
ferror 检测读写错误 错误处理
clearerr 清除错误标志 错误恢复

文件打开方式速查

模式 文件必须存在 清空原内容 追加
"r"
"w"
"a"
"r+"
"w+"
"a+"

b 后缀(如 "rb""wb")表示以二进制模式打开。

易错点汇总

Danger: 常见错误

  1. 忘记检查 fopen 返回值:文件不存在时 fopen 返回 NULL,直接使用会导致程序崩溃
  2. 忘记 fclose:缓冲区数据可能丢失,文件内容不完整
  3. "w" 模式覆盖数据:用 "w" 打开已有文件会清空原有内容,想追加应用 "a"
  4. fgets 的 n 参数:实际只读 n-1 个字符(最后一个位置留给 '\0'
  5. 文本模式下用 fseek:文本文件中换行符转换可能导致定位不准,fseek 推荐用于二进制文件
  6. 混淆文件指针和文件位置标记FILE *fp 是指向文件信息区的指针(不变),文件位置标记是记录当前读写位置的内部变量(随读写移动)

相关笔记