C 语言文件操作全攻略:从基础读写到随机访问与缓冲区原理

在 C 语言程序开发中,数据的持久化存储是不可或缺的核心能力。程序运行时产生的所有数据都存储在内存中,一旦程序退出或系统断电,这些数据就会永久丢失。而文件操作正是解决这一问题的关键,它允许我们将数据写入磁盘文件,在需要时再读取回来,实现数据的长期保存和跨程序共享。本文将系统讲解 C 语言文件操作的全部核心知识,从文件的基本概念到高级的随机读写,再到容易被忽略的文件缓冲区原理,帮你全面掌握这一基础且重要的技能。

一、为什么需要文件操作?

我们之前编写的所有程序,数据都是存储在内存中的:

  • 变量、数组、结构体等数据结构都在栈或堆上分配空间
  • 程序运行时可以读写这些数据,但程序结束后,操作系统会回收所有内存资源
  • 再次运行程序时,无法获取上一次运行产生的任何数据

这种临时性的存储方式无法满足很多实际需求,比如:

  • 保存用户的配置信息和程序状态
  • 存储大量的业务数据(如学生信息、商品库存)
  • 实现程序之间的数据交换
  • 记录程序运行日志以便排查问题

文件操作的出现完美解决了这些问题,它将数据存储在磁盘等持久化存储介质上,即使程序退出或计算机重启,数据依然存在。

二、文件的基本概念

2.1 文件的分类

从程序设计的角度,我们通常将文件分为两大类:

  1. 程序文件 :存储程序代码的文件,包括源程序文件(.c)、目标文件(Windows 下.obj)、可执行程序(Windows 下.exe)等
  2. 数据文件:存储程序运行时读写的数据的文件,这也是本章讨论的重点

2.2 文件名

每个文件都有一个唯一的文件标识,也就是我们常说的文件名,它由三部分组成:

cpp 复制代码
文件路径 + 文件名主干 + 文件后缀

例如:C:\Luminous\code\student.txt

  • 文件路径:C:\Luminous\code\,指定文件在磁盘上的位置
  • 文件名主干:student,用于区分不同的文件
  • 文件后缀:.txt,标识文件的类型

三、二进制文件与文本文件

根据数据在磁盘上的存储方式,数据文件可以分为文本文件二进制文件两种。

3.1 核心区别

  • 文本文件:数据以 ASCII 码的形式存储,每个字符占用一个字节,文件内容可以直接用文本编辑器打开查看
  • 二进制文件:数据直接以内存中的二进制形式存储,不进行任何转换,文件内容无法直接用文本编辑器查看

3.2 存储示例

以整数10000为例:

  • 文本文件存储:占用 5 个字节,分别存储字符 '1'、'0'、'0'、'0'、'0' 的 ASCII 码
  • 二进制文件存储:占用 4 个字节,直接存储整数 10000 的二进制表示00000000 00000000 00100111 00010000

二进制文件写入示例

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

int main()
{
    int a = 10000;
    FILE* Luminous = fopen("test.bin", "wb");
    fwrite(&a, 4, 1, Luminous); // 以二进制形式写入文件
    fclose(Luminous);
    Luminous = NULL;

    return 0;
}

运行后生成的test.bin文件用文本编辑器打开会显示乱码,但用二进制编辑器可以看到正确的十六进制值10 27 00 00(小端存储)。

四、文件的打开与关闭

C 语言中所有的文件操作都遵循 "先打开,后操作,最后关闭" 的流程。

4.1 流与标准流

为了统一对不同外部设备(键盘、显示器、磁盘、打印机等)的操作,C 语言抽象出了的概念。我们可以把流想象成一个数据通道,数据通过流在程序和外部设备之间传输。

C 语言程序在启动时会自动打开三个标准流:

  • stdin:标准输入流,默认关联键盘,scanf函数从该流读取数据
  • stdout:标准输出流,默认关联显示器,printf函数向该流写入数据
  • stderr:标准错误流,默认关联显示器,用于输出错误信息

这三个流的类型都是FILE*,也就是文件指针。

4.2 文件指针

每个被打开的文件,系统都会在内存中创建一个对应的文件信息区 ,用于存储文件的名称、状态、当前读写位置等信息。这个文件信息区是一个结构体,类型名为FILE

我们通过一个FILE*类型的指针来维护这个文件信息区,进而操作对应的文件:

cpp 复制代码
FILE* pf; // 定义一个文件指针变量

4.3 fopen:打开文件

函数原型:

cpp 复制代码
FILE* fopen(const char* filename, const char* mode);
  • filename:要打开的文件名,可以是相对路径或绝对路径
  • mode:文件的打开方式,决定了我们可以对文件进行哪些操作
  • 返回值:打开成功返回指向文件信息区的指针,失败返回NULL

必须检查返回值 :文件打开可能会因为各种原因失败(如文件不存在、权限不足、磁盘已满等),如果直接对NULL指针进行操作,会导致程序崩溃。

4.4 文件打开模式大全

模式 含义 文件不存在时 文件存在时 读写位置
"r" 只读文本文件 出错 保留原有内容 文件开头
"w" 只写文本文件 创建新文件 清空原有内容 文件开头
"a" 追加文本文件 创建新文件 保留原有内容 文件末尾
"rb" 只读二进制文件 出错 保留原有内容 文件开头
"wb" 只写二进制文件 创建新文件 清空原有内容 文件开头
"ab" 追加二进制文件 创建新文件 保留原有内容 文件末尾
"r+" 读写文本文件 出错 保留原有内容 文件开头
"w+" 读写文本文件 创建新文件 清空原有内容 文件开头
"a+" 读写文本文件 创建新文件 保留原有内容 文件末尾
"rb+" 读写二进制文件 出错 保留原有内容 文件开头
"wb+" 读写二进制文件 创建新文件 清空原有内容 文件开头
"ab+" 读写二进制文件 创建新文件 保留原有内容 文件末尾

4.5 fclose:关闭文件

函数原型:

cpp 复制代码
int fclose(FILE* stream);
  • stream:指向要关闭的文件的指针
  • 返回值:关闭成功返回 0,失败返回EOF

重要注意事项

  1. 文件使用完毕后必须关闭,否则会导致资源泄漏,同时可能造成数据丢失
  2. 关闭文件后,原文件指针会变成野指针,必须手动置为NULL

完整的文件打开与关闭示例

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>

int main()
{
    FILE* fp = fopen("test.txt", "r");
    if (fp == NULL)
    {
        perror("fopen failed"); // 打印详细的错误信息
        return 1;
    }

    printf("文件打开成功\n");

    // 这里进行文件读写操作

    fclose(fp); // 关闭文件
    fp = NULL; // 置空指针

    return 0;
}

五、文件的顺序读写

顺序读写是指按照文件内容的先后顺序依次进行读写操作,这是最常用的文件操作方式。C 语言提供了多组函数用于不同类型数据的顺序读写。

5.1 字符读写:fgetc 和 fputc

fputc:写入一个字符

函数原型:

cpp 复制代码
int fputc(int character, FILE* stream);
  • 功能:将一个字符写入指定的输出流
  • 返回值:成功返回写入的字符,失败返回EOF

示例:写入 26 个英文字母到文件

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

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

    for (char ch = 'a'; ch <= 'z'; ch++)
    {
        fputc(ch, fp);
    }

    fclose(fp);
    fp = NULL;
    return 0;
}
fgetc:读取一个字符

函数原型:

cpp 复制代码
int fgetc(FILE* stream);
  • 功能:从指定的输入流读取一个字符
  • 返回值:成功返回读取的字符,到达文件末尾或发生错误返回EOF

示例:读取文件中的所有字符并打印

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

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

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

    fclose(fp);
    fp = NULL;
    return 0;
}

5.2 字符串读写:fgets 和 fputs

fputs:写入一个字符串

函数原型:

cpp 复制代码
int fputs(const char* str, FILE* stream);
  • 功能:将一个字符串写入指定的输出流(不包含结尾的 '\0')
  • 返回值:成功返回非负整数,失败返回EOF

示例

cpp 复制代码
fputs("Hello, World!\n", fp);
fputs("C语言文件操作\n", fp);
fgets:读取一个字符串

函数原型:

cpp 复制代码
char* fgets(char* str, int num, FILE* stream);
  • 功能:从指定的输入流读取字符串,最多读取num-1个字符,遇到换行符或文件末尾停止
  • 特点:会将读取到的换行符 '\n' 也存入字符串中,并在末尾自动添加 '\0'
  • 返回值:成功返回str,到达文件末尾或发生错误返回NULL

示例

cpp 复制代码
char buf[1024];
while (fgets(buf, sizeof(buf), fp) != NULL)
{
    printf("%s", buf);
}

5.3 格式化读写:fscanf 和 fprintf

这两个函数与我们熟悉的scanfprintf用法几乎完全相同,只是多了一个文件指针参数,可以对任意文件流进行格式化读写。

fprintf:格式化写入

函数原型:

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

示例:将结构体数据写入文件

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

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

int main()
{
    struct Student s = {"张三", 20, 95.5f};
    FILE* fp = fopen("student.txt", "w");
    if (fp == NULL)
    {
        perror("fopen");
        return 1;
    }

    fprintf(fp, "%s %d %.1f", s.name, s.age, s.score);

    fclose(fp);
    fp = NULL;
    return 0;
}
fscanf:格式化读取

函数原型:

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

示例:从文件中读取结构体数据

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

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

int main()
{
    struct Student s = {0};
    FILE* fp = fopen("student.txt", "r");
    if (fp == NULL)
    {
        perror("fopen");
        return 1;
    }

    fscanf(fp, "%s %d %f", s.name, &s.age, &s.score);
    printf("姓名:%s,年龄:%d,成绩:%.1f\n", s.name, s.age, s.score);

    fclose(fp);
    fp = NULL;
    return 0;
}

5.4 数据块读写:fread 和 fwrite

这两个函数用于以二进制形式读写整块数据,适合读写数组、结构体等复杂数据类型,效率比格式化读写高。

fwrite:二进制写入

函数原型:

cpp 复制代码
size_t fwrite(const void* ptr, size_t size, size_t count, FILE* stream);
  • ptr:指向要写入的数据的指针
  • size:每个数据项的大小(字节)
  • count:要写入的数据项的数量
  • 返回值:实际成功写入的数据项数量

示例:写入一个整型数组到二进制文件

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

int main()
{
    int arr[] = {1, 2, 3, 4, 5};
    FILE* fp = fopen("array.bin", "wb");
    if (fp == NULL)
    {
        perror("fopen");
        return 1;
    }

    fwrite(arr, sizeof(int), 5, fp);

    fclose(fp);
    fp = NULL;
    return 0;
}
fread:二进制读取

函数原型:

cpp 复制代码
size_t fread(void* ptr, size_t size, size_t count, FILE* stream);
  • 返回值:实际成功读取的数据项数量

示例:从二进制文件读取整型数组

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

int main()
{
    int arr[5] = {0};
    FILE* fp = fopen("array.bin", "rb");
    if (fp == NULL)
    {
        perror("fopen");
        return 1;
    }

    size_t n = fread(arr, sizeof(int), 5, fp);
    printf("成功读取%d个整数\n", n);
    for (int i = 0; i < n; i++)
    {
        printf("%d ", arr[i]);
    }

    fclose(fp);
    fp = NULL;
    return 0;
}

5.5 重要函数对比

C 语言中有三组非常相似的格式化输入输出函数,很多初学者容易混淆,这里做一个清晰的对比:

函数 输入 / 输出源 用途
scanf 标准输入流 (stdin) 从键盘读取格式化数据
printf 标准输出流 (stdout) 向屏幕输出格式化数据
fscanf 任意输入流 从文件或键盘读取格式化数据
fprintf 任意输出流 向文件或屏幕输出格式化数据
sscanf 字符串 从内存中的字符串解析格式化数据
sprintf 字符串 将格式化数据写入内存中的字符串

sprintf 和 sscanf 示例

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

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

int main()
{
    struct Student s1 = {"李四", 21, 88.0f};
    char buf[100] = {0};

    // 将结构体数据格式化为字符串
    sprintf(buf, "%s %d %.1f", s1.name, s1.age, s1.score);
    printf("格式化后的字符串:%s\n", buf);

    // 从字符串中解析出结构体数据
    struct Student s2 = {0};
    sscanf(buf, "%s %d %f", s2.name, &s2.age, &s2.score);
    printf("解析结果:%s %d %.1f\n", s2.name, s2.age, s2.score);

    return 0;
}

六、文件的随机读写

顺序读写只能从文件开头或末尾开始依次操作,而随机读写允许我们在文件的任意位置进行读写,大大提高了文件操作的灵活性。

6.1 fseek:定位文件指针

函数原型:

cpp 复制代码
int fseek(FILE* stream, long int offset, int origin);
  • offset:偏移量,正数表示向后偏移,负数表示向前偏移
  • origin:起始位置,有三个可选值:
    • SEEK_SET:文件开头
    • SEEK_CUR:文件指针当前位置
    • SEEK_END:文件末尾

示例:修改文件中的指定内容

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

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

    fputs("This is an apple.", fp);
    fseek(fp, 9, SEEK_SET); // 从文件开头向后偏移9个字节
    fputs(" sam", fp); // 写入内容覆盖原有数据

    fclose(fp);
    fp = NULL;
    return 0;
}

运行后文件内容变为:This is a sample.

6.2 ftell:获取当前偏移量

函数原型:

cpp 复制代码
long int ftell(FILE* stream);
  • 功能:返回文件指针相对于文件开头的当前偏移量
  • 常用用途:获取文件的大小

示例:获取文件大小

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

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

    fseek(fp, 0, SEEK_END); // 将文件指针移到文件末尾
    long size = ftell(fp); // 获取当前偏移量,即文件大小
    printf("文件大小:%ld字节\n", size);

    fclose(fp);
    fp = NULL;
    return 0;
}

6.3 rewind:回到文件开头

函数原型:

cpp 复制代码
void rewind(FILE* stream);
  • 功能:将文件指针重新定位到文件的起始位置
  • 等价于:fseek(stream, 0, SEEK_SET);

示例:先写后读同一个文件

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

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

    // 写入数据
    for (char ch = 'A'; ch <= 'Z'; ch++)
    {
        fputc(ch, fp);
    }

    rewind(fp); // 回到文件开头

    // 读取数据
    char buf[27] = {0};
    fread(buf, 1, 26, fp);
    printf("%s\n", buf);

    fclose(fp);
    fp = NULL;
    return 0;
}

七、文件读取结束的正确判定

很多初学者会错误地使用feof函数来判断文件是否读取结束,这是一个非常常见的误区。

7.1 feof 和 ferror 的正确用法

  • feof(FILE* stream):检测流是否到达文件末尾,到达返回非 0 值,否则返回 0
  • ferror(FILE* stream):检测流是否发生读写错误,发生错误返回非 0 值,否则返回 0

核心原则feof函数是在读取函数返回 EOF 之后 ,用来判断是因为到达文件末尾而结束,还是因为发生错误而结束。不能用feof直接作为循环条件来读取文件。

错误写法

cpp 复制代码
// 错误!feof会在文件最后一个字符读取后才返回真,导致多读取一次
while (!feof(fp))
{
    int ch = fgetc(fp);
    putchar(ch);
}

正确写法

cpp 复制代码
int ch;
while ((ch = fgetc(fp)) != EOF)
{
    putchar(ch);
}

// 读取结束后,判断是正常结束还是错误结束
if (feof(fp))
{
    printf("文件读取正常结束\n");
}
else if (ferror(fp))
{
    printf("文件读取发生错误\n");
}

八、文件缓冲区原理

很多人可能会遇到这样的情况:向文件写入数据后,立即打开文件却发现没有内容,这是因为 C 语言采用了缓冲文件系统

8.1 什么是文件缓冲区

ANSI C 标准规定,系统会自动在内存中为每个正在使用的文件开辟一块缓冲区:

  • 输出缓冲区:程序向文件写入数据时,先将数据写入输出缓冲区,缓冲区满了之后才会一次性写入磁盘
  • 输入缓冲区:程序从文件读取数据时,先将磁盘上的数据读入输入缓冲区,然后再从缓冲区逐个读取数据到程序变量

缓冲区的存在大大提高了文件操作的效率,因为磁盘的读写速度比内存慢得多,频繁的磁盘读写会严重影响程序性能。

8.2 fflush:强制刷新缓冲区

函数原型:

cpp 复制代码
int fflush(FILE* stream);
  • 功能:强制将输出缓冲区中的数据立即写入磁盘
  • 参数为NULL时,会刷新所有打开的输出流

缓冲区演示示例

cpp 复制代码
#include <stdio.h>
#include <unistd.h> // 用于sleep函数

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

    fputs("Hello, Buffer!", fp); // 数据先写入缓冲区
    printf("数据已写入缓冲区,现在打开文件看不到内容\n");
    sleep(5); // 睡眠5秒

    fflush(fp); // 强制刷新缓冲区,数据写入磁盘
    printf("缓冲区已刷新,现在打开文件可以看到内容了\n");
    sleep(5);

    fclose(fp); // 关闭文件时也会自动刷新缓冲区
    fp = NULL;
    return 0;
}

8.3 重要结论

因为缓冲区的存在,我们在操作文件时必须注意:

  1. 文件操作结束后一定要调用fclose关闭文件,它会自动刷新缓冲区
  2. 如果需要立即将数据写入磁盘,可以调用fflush强制刷新
  3. 程序崩溃时,缓冲区中的数据会丢失,无法写入磁盘

九、文件更新模式详解

在实际开发中,我们经常需要同时读写同一个文件,这时候就需要使用r+w+a+这三种更新模式。它们的行为有很大区别,使用时一定要注意:

模式 文件不存在 文件存在 初始指针位置 写入特点 典型用途
"r+" 打开失败 保留内容 文件开头 可覆盖原有数据 修改文件部分内容
"w+" 创建新文件 清空内容 文件开头 从头写入 创建新文件或完全重写
"a+" 创建新文件 保留内容 文件末尾 只能在末尾追加 日志记录

使用更新模式的注意事项

  1. 写完文件后要继续读文件,必须先调用fflush刷新缓冲区,或者用fseek/rewind重新定位文件指针
  2. 读完文件后要继续写文件,必须用fseek/rewind重新定位文件指针

十、总结

文件操作是 C 语言程序开发中不可或缺的技能,它让我们能够实现数据的持久化存储。本文我们系统学习了:

  1. 文件操作的必要性:解决内存数据临时性的问题
  2. 文件的基本概念:分类、文件名、二进制文件与文本文件的区别
  3. 文件的打开与关闭:fopenfclose的用法,各种打开模式的区别
  4. 文件的顺序读写:字符、字符串、格式化、数据块四种读写方式
  5. 文件的随机读写:fseekftellrewind三个核心函数
  6. 文件读取结束的正确判定:feofferror的正确用法
  7. 文件缓冲区原理:理解缓冲区的作用和刷新时机
  8. 文件更新模式:r+w+a+的区别和使用注意事项

文件操作的核心原则

  • 打开文件必须检查返回值
  • 读写操作要符合文件的打开模式
  • 文件使用完毕必须及时关闭
  • 关闭文件后立即将指针置为 NULL
  • 不要用feof直接作为循环条件读取文件

掌握了这些知识,你就能够熟练地进行各种文件操作,写出稳定、高效的 C 语言程序了。

相关推荐
Brilliantwxx1 小时前
【C++】Stack和Queue(初认识和算法题OJ)
开发语言·c++·笔记·算法
傻瓜搬砖人1 小时前
c语言绿皮书第三版第十章习题
c语言·开发语言·算法
三块可乐两块冰1 小时前
rag笔记1
笔记
会编程的土豆1 小时前
Gin 框架入门笔记
笔记·gin
星恒随风1 小时前
C语言算法复杂度详解:时间复杂度与空间复杂度一篇讲透
c语言·算法
傻瓜搬砖人1 小时前
c语言绿皮书第三版第十一章习题
c语言·开发语言·算法·谭浩强·绿皮书第三版
Heartache boy2 小时前
野火STM32_HAL库版课程笔记-DWT应用与DHT11温湿度传感器
笔记·stm32·单片机·嵌入式硬件
hmbbcsm9 小时前
关于transformors库的学习笔记
笔记·学习
xqqxqxxq10 小时前
Java AI智能P图工具技术笔记
java·人工智能·笔记