C语言笔记归纳20:文件操作

文件操作

目录

文件操作

1. 🌟 为什么需要文件操作?(内存数据的 “永久存储” 方案)

2. 📌 什么是文件?(程序文件 vs 数据文件)

2.1 程序文件

2.2 数据文件

文件名(文件的 “唯一标识”)

3. 📊 文本文件 vs 二进制文件(数据存储的两种形式)

3.1 核心定义

3.2 直观示例:整数10000的存储对比

3.3 代码验证(文本 vs 二进制存储)

4. 🚪 文件的打开与关闭(核心操作:fopen/fclose)

4.1 流与标准流(默认打开的 3 个流)

4.2 文件指针(FILE*:文件的 “身份证”)

4.3 文件打开方式详解(表格汇总)

4.4 打开与关闭文件的正确示例

易错点标注:

5. 📝 文件的顺序读写(8 个核心函数 + 对比)

5.1 字符读写:fputc/fgetc(单个字符操作)

fputc:字符输出函数(写字符)

fgetc:字符输入函数(读字符)

5.2 行读写:fputs/fgets(整行数据操作)

fputs:文本行输出函数(写整行)

fgets:文本行输入函数(读整行)

易错点:

5.3 格式化读写:fprintf/fscanf(结构体 / 多类型数据)

fprintf:格式化输出函数(写多类型数据)

fscanf:格式化输入函数(读多类型数据)

易错点:

5.4 二进制读写:fwrite/fread(高效存储)

fwrite:二进制输出函数(写二进制数据)

fread:二进制输入函数(读二进制数据)

5.5 拓展:三组格式化函数对比

实战示例:sscanf/sprintf

6. 🔀 文件的随机读写(灵活定位:fseek/ftell/rewind)

6.1 fseek:定位文件指针

实战示例:定位读取文件内容

6.2 ftell:获取文件指针偏移量

6.3 rewind:重置文件指针到开头

7. ❗ 文件读取结果判定(feof/ferror:区分 “文件尾” 和 “读错误”)

7.1 feof:判断是否读到文件尾

7.2 ferror:判断是否发生读错误

实战示例:正确判定读取结束原因

异常情况测试(故意出错):

8. 📦 文件缓冲区(为什么数据没立刻写到硬盘?)

缓冲区的作用:

实战验证缓冲区存在:

关键结论:

9. 🚀 实战:文件拷贝程序(文本 / 二进制通用)

核心思路:

代码实现:

优化:增大缓冲区提升效率

10. ⚠️ 常见文件操作易错点(避坑指南)

11. ✅ 文件操作最佳实践(总结)


✨引言:

在 C 语言中,程序运行时的数据默认存储在内存中 —— 一旦程序退出,内存被系统回收,数据就会永久丢失。比如你输入一个数字10,程序退出后再次运行,上次的10就不见了。

如果想让数据持久化保存(比如存到硬盘),就需要用到「文件操作」。本文将从 “为什么用文件” 到 “缓冲区原理”,用通俗比喻 + 实战代码,拆解文件操作的核心知识点,包括文件类型、打开关闭、顺序 / 随机读写、错误判定,还会补充大量细节和实战案例,帮你彻底掌握文件操作!

1. 🌟 为什么需要文件操作?(内存数据的 “永久存储” 方案)

先看一个简单的例子:

#include <stdio.h>
int main() {
    int n = 0;
    scanf("%d", &n); // 输入10
    printf("%d", n); // 打印10
    return 0;
}
  • 程序运行时,n=10存储在内存栈区
  • 程序退出后,栈区内存被系统回收,10消失;
  • 再次运行程序,无法获取上次的10

文件操作的核心价值

将数据从内存写入硬盘(或从硬盘读入内存),实现数据的 “持久化存储”。就像把临时放在桌上的文件(内存),放进抽屉(硬盘)永久保存。

2. 📌 什么是文件?(程序文件 vs 数据文件)

放在硬盘上的数据集统称为 “文件”,在 C 语言中分为两类:

2.1 程序文件

  • 用于存放程序代码和编译相关的文件;
  • 例如:源文件(.c)、目标文件(.obj)、可执行文件(.exe)。

2.2 数据文件

  • 用于存放程序运行时读写的数据(不是程序本身);
  • 例如:程序读取的配置文件(config.txt)、程序输出的日志文件(log.txt)。

文件名(文件的 “唯一标识”)

一个完整的文件名包含三部分:文件路径 + 文件名主干 + 文件后缀示例:C:\code\test.txt

  • 路径:C:\code\(文件所在位置);
  • 主干:test(文件名);
  • 后缀:.txt(文件类型,文本文件)。

3. 📊 文本文件 vs 二进制文件(数据存储的两种形式)

根据数据的存储方式,数据文件分为「文本文件」和「二进制文件」,核心区别是是否将内存中的二进制数据转换为 ASCII 码。

3.1 核心定义

类型存储规则特点
文本文件内存中的二进制数据→转换为 ASCII 码→存储到硬盘可被记事本打开,人类可读
二进制文件内存中的二进制数据→直接存储到硬盘(不转换)不可被记事本直接读取(乱码),存储高效

3.2 直观示例:整数10000的存储对比

  • 内存中10000的二进制:00000000 00000000 00100111 00010000(4 字节);
  • 文本文件存储:转换为 5 个 ASCII 码字符('1'→49、'0'→48、'0'→48、'0'→48、'0'→48),占用 5 字节;
  • 二进制文件存储:直接存储 4 字节二进制数据,占用 4 字节。

3.3 代码验证(文本 vs 二进制存储)

#include <stdio.h>
int main() {
    int a = 10000;
    // 1. 二进制存储(wb模式)
    FILE* pf1 = fopen("binary.txt", "wb");
    if (pf1 != NULL) {
        fwrite(&a, 4, 1, pf1); // 直接写二进制数据(4字节)
        fclose(pf1);
        pf1 = NULL;
    }

    // 2. 文本存储(w模式)
    FILE* pf2 = fopen("text.txt", "w");
    if (pf2 != NULL) {
        fprintf(pf2, "%d", a); // 转换为ASCII码存储(5字节)
        fclose(pf2);
        pf2 = NULL;
    }
    return 0;
}
  • 用记事本打开binary.txt:显示乱码(二进制数据);
  • 用记事本打开text.txt:显示10000(ASCII 码,人类可读)。

4. 🚪 文件的打开与关闭(核心操作:fopen/fclose)

文件操作遵循 “三段论”:打开文件→读写文件→关闭文件

就像 “喝水”:打开水瓶→喝水→关闭水瓶。

4.1 流与标准流(默认打开的 3 个流)

程序要和外部设备(键盘、屏幕、硬盘文件)交互,需要通过 “流”(数据传输的通道)。C 语言启动时,会默认打开 3 个标准流,无需手动打开:

标准流作用对应设备常用函数
stdin标准输入流(读取数据)键盘scanf、fgetc
stdout标准输出流(输出数据)屏幕printf、fputc
stderr标准错误流(输出错误信息)屏幕perror、fprintf

💡 比喻:流就像 “数据线”,连接程序和外部设备,数据通过 “数据线” 传输。

4.2 文件指针(FILE*:文件的 “身份证”)

每个被打开的文件,系统都会在内存中开辟一块 “文件信息区”,存储文件名、状态、当前读写位置等信息。这个信息区被封装在FILE结构体中,通过FILE*类型的指针(文件指针)来访问。

// FILE* 指针指向文件信息区,间接操作文件
FILE* pf1; // 指向文件1的信息区
FILE* pf2; // 指向文件2的信息区
  • 打开文件时,fopen返回文件指针,指向该文件的信息区;
  • 关闭文件时,fclose释放文件信息区,指针需置空(避免野指针)。

4.3 文件打开方式详解(表格汇总)

打开文件的核心函数是fopen,第二个参数mode指定打开方式,不同方式决定了文件的读写权限和创建规则:

打开方式类型核心权限文件不存在时适用场景
"r"文本只读(输入)出错(返回 NULL)读取已存在的文本文件
"w"文本只写(输出)创建新文件覆盖 / 创建文本文件并写入
"a"文本追加(在文件尾写入)创建新文件向文本文件尾添加数据
"r+"文本读写(先读)出错读写已存在的文本文件
"w+"文本读写(先写)创建新文件覆盖 / 创建文本文件并读写
"a+"文本读写(追加 + 读)创建新文件读写文本文件,写在尾部
"rb"二进制只读(输入)出错读取已存在的二进制文件
"wb"二进制只写(输出)创建新文件覆盖 / 创建二进制文件并写入
"ab"二进制追加(在文件尾写入)创建新文件向二进制文件尾添加数据
"rb+"二进制读写(先读)出错读写已存在的二进制文件
"wb+"二进制读写(先写)创建新文件覆盖 / 创建二进制文件并读写
"ab+"二进制读写(追加 + 读)创建新文件读写二进制文件,写在尾部

4.4 打开与关闭文件的正确示例

#include <stdio.h>
int main() {
    // 1. 打开文件:读文本文件("r"模式)
    FILE* pf = fopen("test.txt", "r");
    
    // 关键:判断文件是否打开成功(避免NULL指针)
    if (pf == NULL) {
        perror("fopen failed"); // 打印错误信息(如:fopen failed: No such file or directory)
        return 1; // 打开失败,退出程序
    }

    // 2. 读写文件(后续讲解)
    // ...

    // 3. 关闭文件(必须!否则内存泄漏、数据丢失)
    fclose(pf);
    pf = NULL; // 指针置空,避免野指针

    return 0;
}
易错点标注:
// ❌ 错误1:未判断打开失败
FILE* pf = fopen("test.txt", "r");
fgetc(pf); // 若pf为NULL,崩溃

// ❌ 错误2:关闭后未置空
fclose(pf);
fputc('a', pf); // 野指针,非法访问

// ❌ 错误3:打开方式与操作不匹配
FILE* pf = fopen("test.txt", "r"); // 只读模式
fputc('a', pf); // 错误:只读模式不能写

5. 📝 文件的顺序读写(8 个核心函数 + 对比)

顺序读写是指从文件开头到结尾,按顺序读写数据(不能跳着来),核心是 8 个函数,分为 4 组:

5.1 字符读写:fputc/fgetc(单个字符操作)

fputc:字符输出函数(写字符)
  • 原型:int fputc(int character, FILE* stream);
  • 功能:将字符character写入stream流(文件 / 屏幕);
  • 返回值:成功返回写入的字符(ASCII 码),失败返回EOF(-1)。
int main() {
    // 打开文件:写文本文件("w"模式)
    FILE* pf = fopen("test.txt", "w");
    if (pf == NULL) {
        perror("fopen");
        return 1;
    }

    // 写字符:单个写入
    fputc('a', pf);
    fputc('b', pf);
    fputc('c', pf);

    // 写字符:循环写入a~z
    for (char ch = 'a'; ch <= 'z'; ch++) {
        fputc(ch, pf); // 写入abcdefghijklmnopqrstuvwxyz
    }

    fclose(pf);
    pf = NULL;
    return 0;
}
  • 结果:test.txt中存储abcabcdefghijklmnopqrstuvwxyz(前 3 个是单个写入,后 26 个是循环写入)。
fgetc:字符输入函数(读字符)
  • 原型:int fgetc(FILE* stream);
  • 功能:从stream流(文件 / 键盘)读取单个字符;
  • 返回值:成功返回字符的 ASCII 码,失败或读到文件尾返回EOF(-1)。
int main() {
    // 打开文件:读文本文件("r"模式)
    FILE* pf = fopen("test.txt", "r");
    if (pf == NULL) {
        perror("fopen");
        return 1;
    }

    // 读字符:循环读取所有字符(直到EOF)
    int ch = 0; // 用int接收,因为EOF是-1(char无法存储)
    while ((ch = fgetc(pf)) != EOF) {
        printf("%c", ch); // 输出:abcabcdefghijklmnopqrstuvwxyz
    }

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

💡 关键:用int接收fgetc的返回值,因为char的范围是-128~127,而EOF-1,若字符是0xFF(ASCII 码 255),会和EOF冲突。

5.2 行读写:fputs/fgets(整行数据操作)

fputs:文本行输出函数(写整行)
  • 原型:int fputs(const char* str, FILE* stream);
  • 功能:将字符串str写入stream流,不自动添加换行符
  • 返回值:成功返回非负数,失败返回EOF
int main() {
    FILE* pf = fopen("test.txt", "w");
    if (pf == NULL) return 1;

    // 写整行:不自动加换行
    fputs("hello world", pf);
    fputs("hello bit", pf); // 结果:hello worldhello bit(同一行)

    // 手动加换行符
    fputs("hello world\n", pf);
    fputs("hello bit\n", pf); // 结果:hello world(行1),hello bit(行2)

    fclose(pf);
    pf = NULL;
    return 0;
}
fgets:文本行输入函数(读整行)
  • 原型:char* fgets(char* str, int num, FILE* stream);
  • 功能:从stream流读取最多num-1个字符(留 1 个存'\0'),遇到'\n'或文件尾停止;
  • 返回值:成功返回str(存储字符串的地址),失败或文件尾返回NULL
int main() {
    FILE* pf = fopen("test.txt", "r");
    if (pf == NULL) return 1;

    char arr[20] = {0};
    // 读第一行:最多读19个字符(num=20)
    fgets(arr, 20, pf);
    printf("%s", arr); // 输出:hello worldhello bit(无换行)

    // 读第二行
    fgets(arr, 20, pf);
    printf("%s", arr); // 输出:hello world(带换行)

    // 循环读取所有行
    while (fgets(arr, 20, pf) != NULL) {
        printf("%s", arr);
    }

    fclose(pf);
    pf = NULL;
    return 0;
}
易错点:
  • fgetsnum必须大于字符串长度 + 1(留'\0'),否则会截断字符串;
  • fgets会读取'\n'并存储在字符串中,打印时无需额外加'\n'

5.3 格式化读写:fprintf/fscanf(结构体 / 多类型数据)

fprintf:格式化输出函数(写多类型数据)
  • 原型:int fprintf(FILE* stream, const char* format, ...);
  • 功能:将多类型数据(int/char/struct 等)按格式写入stream流;
  • 对比printfprintf默认写入stdout(屏幕),fprintf可指定流(文件 / 屏幕)。
// 定义结构体
struct Student {
    char name[20];
    int age;
    float score;
};

int main() {
    struct Student s = {"张三", 20, 65.5f};
    // 打开文件:写文本文件
    FILE* pf = fopen("student.txt", "w");
    if (pf == NULL) return 1;

    // 写结构体数据到文件
    fprintf(pf, "%s %d %.1f", s.name, s.age, s.score);
    // 写屏幕(等价于printf)
    fprintf(stdout, "%s %d %.1f\n", s.name, s.age, s.score);

    fclose(pf);
    pf = NULL;
    return 0;
}
  • 结果:student.txt中存储张三 20 65.5,屏幕输出相同内容。
fscanf:格式化输入函数(读多类型数据)
  • 原型:int fscanf(FILE* stream, const char* format, ...);
  • 功能:从stream流按格式读取多类型数据;
  • 对比scanfscanf默认读取stdin(键盘),fscanf可指定流(文件 / 键盘)。
struct Student {
    char name[20];
    int age;
    float score;
};

int main() {
    struct Student s = {0};
    // 打开文件:读文本文件(注意:必须用"r"模式,不能用"w")
    FILE* pf = fopen("student.txt", "r");
    if (pf == NULL) return 1;

    // 从文件读取数据到结构体
    fscanf(pf, "%s %d %f", s.name, &s.age, &s.score);
    // 从键盘读取(等价于scanf)
    fscanf(stdin, "%s %d %f", s.name, &s.age, &s.score);

    // 打印验证
    printf("%s %d %.1f\n", s.name, s.age, s.score);

    fclose(pf);
    pf = NULL;
    return 0;
}
易错点:
  • fscanf读取字符串时,name是数组名(本身是地址),无需加&
  • 打开文件时,读操作必须用"r"/"r+"模式,写操作必须用"w"/"w+"/"a"/"a+"模式,否则操作失败。

5.4 二进制读写:fwrite/fread(高效存储)

二进制读写直接操作内存中的二进制数据,不转换为 ASCII 码,存储效率高(占用空间小、速度快),适用于结构体、数组等复杂数据。

fwrite:二进制输出函数(写二进制数据)
  • 原型:size_t fwrite(const void* ptr, size_t size, size_t count, FILE* stream);
  • 参数:
    • ptr:指向要写入的数据(数组 / 结构体地址);
    • size:每个数据的字节数;
    • count:数据的个数;
    • stream:目标流(只能是文件流,不能是 stdin/stdout);
  • 返回值:成功写入的数据个数。
int main() {
    int arr[] = {1, 2, 3, 4, 5};
    // 打开文件:写二进制文件("wb"模式)
    FILE* pf = fopen("binary_arr.txt", "wb");
    if (pf == NULL) return 1;

    // 写数组到文件:5个int,每个4字节
    size_t ret = fwrite(arr, sizeof(int), 5, pf);
    printf("成功写入%d个数据\n", ret); // 输出:成功写入5个数据

    fclose(pf);
    pf = NULL;
    return 0;
}
fread:二进制输入函数(读二进制数据)
  • 原型:size_t fread(void* ptr, size_t size, size_t count, FILE* stream);
  • 功能:从文件流读取二进制数据到ptr指向的空间;
  • 返回值:成功读取的数据个数(小于count表示读取结束或失败)。
int main() {
    int arr[5] = {0};
    // 打开文件:读二进制文件("rb"模式)
    FILE* pf = fopen("binary_arr.txt", "rb");
    if (pf == NULL) return 1;

    // 读文件到数组:最多读5个int
    size_t ret = fread(arr, sizeof(int), 5, pf);
    printf("成功读取%d个数据:", ret);
    for (int i = 0; i < ret; i++) {
        printf("%d ", arr[i]); // 输出:1 2 3 4 5
    }

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

5.5 拓展:三组格式化函数对比

函数功能适用场景
scanf从标准输入流(stdin)读取格式化数据键盘输入→程序
fscanf从指定输入流(文件 / 键盘)读取格式化数据文件 / 键盘输入→程序
sscanf从字符串中读取格式化数据字符串→程序
printf向标准输出流(stdout)输出格式化数据程序→屏幕输出
fprintf向指定输出流(文件 / 屏幕)输出格式化数据程序→文件 / 屏幕输出
sprintf将格式化数据转换为字符串程序→字符串
实战示例:sscanf/sprintf
#include <stdio.h>
struct Student {
    char name[20];
    int age;
    float score;
};

int main() {
    char buf[100] = {0};
    struct Student s = {"张三", 20, 65.5f};
    struct Student t = {0};

    // 1. sprintf:将结构体数据转为字符串
    sprintf(buf, "%s %d %.1f", s.name, s.age, s.score);
    printf("字符串:%s\n", buf); // 输出:字符串:张三 20 65.5

    // 2. sscanf:从字符串读取数据到结构体
    sscanf(buf, "%s %d %f", t.name, &t.age, &t.score);
    printf("结构体:%s %d %.1f\n", t.name, t.age, t.score); // 输出:张三 20 65.5

    return 0;
}

6. 🔀 文件的随机读写(灵活定位:fseek/ftell/rewind)

顺序读写只能从开头到结尾按顺序操作,随机读写可以通过 “定位文件指针”,跳转到文件任意位置读写(比如直接读第 5 个字符、修改中间数据)。

6.1 fseek:定位文件指针

  • 原型:int fseek(FILE* stream, long int offset, int origin);
  • 功能:根据origin(起始位置)和offset(偏移量),定位文件指针;
  • 参数说明:
    • origin:起始位置(3 种选择):
      • SEEK_SET:文件开头(0);
      • SEEK_CUR:文件指针当前位置;
      • SEEK_END:文件末尾;
    • offset:偏移量(正数→向后移,负数→向前移)。
实战示例:定位读取文件内容

假设test.txt中存储abcdefghi(9 个字符,索引 0~8):

#include <stdio.h>
int main() {
    FILE* pf = fopen("test.txt", "r");
    if (pf == NULL) return 1;

    int ch = 0;
    // 1. 读第一个字符(a)
    ch = fgetc(pf);
    printf("%c\n", ch); // 输出:a

    // 2. 从当前位置(a后)向后移4个字符→f
    fseek(pf, 4, SEEK_CUR);
    ch = fgetc(pf);
    printf("%c\n", ch); // 输出:f

    // 3. 从文件开头向后移5个字符→f
    fseek(pf, 5, SEEK_SET);
    ch = fgetc(pf);
    printf("%c\n", ch); // 输出:f

    // 4. 从文件末尾向前移4个字符→f
    fseek(pf, -4, SEEK_END);
    ch = fgetc(pf);
    printf("%c\n", ch); // 输出:f

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

6.2 ftell:获取文件指针偏移量

  • 原型:long int ftell(FILE* stream);
  • 功能:返回文件指针相对于文件开头的偏移量(字节数)。
int main() {
    FILE* pf = fopen("test.txt", "r");
    if (pf == NULL) return 1;

    fseek(pf, 0, SEEK_END); // 定位到文件末尾
    long int len = ftell(pf); // 获取偏移量(文件长度)
    printf("文件长度:%ld字节\n", len); // 输出:9字节

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

6.3 rewind:重置文件指针到开头

  • 原型:void rewind(FILE* stream);
  • 功能:将文件指针重置到文件开头。
int main() {
    FILE* pf = fopen("test.txt", "r");
    if (pf == NULL) return 1;

    int ch = fgetc(pf);
    printf("%c\n", ch); // 输出:a

    fseek(pf, -4, SEEK_END);
    ch = fgetc(pf);
    printf("%c\n", ch); // 输出:f

    rewind(pf); // 重置到开头
    ch = fgetc(pf);
    printf("%c\n", ch); // 输出:a

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

7. ❗ 文件读取结果判定(feof/ferror:区分 “文件尾” 和 “读错误”)

文件读取结束有两种原因:

  1. 正常结束:读到文件末尾(EOF);
  2. 异常结束:读取过程中发生错误(如文件损坏、权限不足)。

仅通过fgetc返回EOF无法区分这两种情况,需要用feofferror函数判定。

7.1 feof:判断是否读到文件尾

  • 原型:int feof(FILE* stream);
  • 功能:检测文件是否因读到末尾而结束;
  • 返回值:是→非 0 值,否→0。

7.2 ferror:判断是否发生读错误

  • 原型:int ferror(FILE* stream);
  • 功能:检测文件是否因错误而结束;
  • 返回值:是→非 0 值,否→0。

实战示例:正确判定读取结束原因

#include <stdio.h>
int main() {
    FILE* pf = fopen("test.txt", "r");
    if (pf == NULL) return 1;

    int ch = 0;
    // 循环读取
    while ((ch = fgetc(pf)) != EOF) {
        printf("%c", ch);
    }
    printf("\n");

    // 判定结束原因
    if (feof(pf)) {
        printf("读取正常结束:已到文件末尾\n");
    } else if (ferror(pf)) {
        perror("读取异常结束");
    }

    fclose(pf);
    pf = NULL;
    return 0;
}
异常情况测试(故意出错):
int main() {
    FILE* pf = fopen("test.txt", "r");
    if (pf == NULL) return 1;

    // 错误操作:只读模式下写数据
    char ch = 'x';
    for (ch = 'a'; ch <= 'z'; ch++) {
        fputc(ch, pf); // 只读模式不允许写,会触发错误
    }

    // 判定错误
    if (ferror(pf)) {
        perror("操作失败"); // 输出:操作失败: Bad file descriptor
    }

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

8. 📦 文件缓冲区(为什么数据没立刻写到硬盘?)

C 语言文件操作存在 “缓冲区” 机制 —— 数据不会直接写入硬盘,而是先存入内存中的缓冲区,当缓冲区满、调用fflushfclose时,才会将数据同步到硬盘。

缓冲区的作用:

  • 减少硬盘 I/O 次数(硬盘读写速度慢,缓冲区批量写入更高效);
  • 比喻:缓冲区就像 “快递驿站”,快递员不会收到一个快递就送一次,而是攒一批再送,提高效率。

实战验证缓冲区存在:

#include <stdio.h>
#include <windows.h> // Sleep函数头文件(Windows)
// #include <unistd.h> // sleep函数头文件(Linux)

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

    fputs("abcdef", pf); // 数据写入缓冲区(未同步到硬盘)
    printf("睡眠10秒:此时打开buffer.txt,无内容\n");
    Sleep(10000); // 睡眠10秒(Windows),Linux用sleep(10)

    fflush(pf); // 手动刷新缓冲区:数据同步到硬盘
    printf("刷新缓冲区后,睡眠10秒:此时打开buffer.txt,有内容\n");
    Sleep(10000);

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

关键结论:

  • 缓冲区由 C 标准库管理,默认大小由编译器决定(通常 4KB/8KB);
  • fflush(stream):手动刷新缓冲区(仅对输出流有效);
  • fclose:关闭文件时自动刷新缓冲区,因此必须关闭文件,否则缓冲区数据可能丢失。

9. 🚀 实战:文件拷贝程序(文本 / 二进制通用)

需求:将source.txt(源文件)的内容拷贝到dest.txt(目标文件),支持文本和二进制文件。

核心思路:

  1. 打开源文件(读模式:"r""rb")和目标文件(写模式:"w""wb");
  2. 循环读取源文件数据,写入目标文件;
  3. 关闭两个文件。

代码实现:

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

// 拷贝函数:src_path(源文件路径),dest_path(目标文件路径)
void file_copy(const char* src_path, const char* dest_path) {
    // 打开源文件(二进制读模式,兼容文本和二进制文件)
    FILE* pfin = fopen(src_path, "rb");
    if (pfin == NULL) {
        perror("打开源文件失败");
        exit(1); // 退出程序
    }

    // 打开目标文件(二进制写模式)
    FILE* pfout = fopen(dest_path, "wb");
    if (pfout == NULL) {
        perror("打开目标文件失败");
        fclose(pfin); // 先关闭源文件,避免内存泄漏
        exit(1);
    }

    // 循环读写:每次读1字节,写1字节(也可增大缓冲区提升效率)
    int ch = 0;
    while ((ch = fgetc(pfin)) != EOF) {
        fputc(ch, pfout);
    }

    // 判定拷贝是否成功
    if (feof(pfin)) {
        printf("拷贝成功!\n");
    } else if (ferror(pfin)) {
        perror("拷贝失败");
    }

    // 关闭文件
    fclose(pfin);
    fclose(pfout);
    pfin = NULL;
    pfout = NULL;
}

int main() {
    // 拷贝文本文件
    file_copy("source.txt", "dest.txt");
    // 拷贝二进制文件(如图片、exe)
    // file_copy("image.jpg", "image_copy.jpg");

    return 0;
}

优化:增大缓冲区提升效率

每次读写 1 字节效率低,可使用数组作为缓冲区,每次读写 1024 字节:

// 优化版:缓冲区大小1024字节
void file_copy_opt(const char* src_path, const char* dest_path) {
    FILE* pfin = fopen(src_path, "rb");
    FILE* pfout = fopen(dest_path, "wb");
    if (pfin == NULL || pfout == NULL) {
        perror("文件打开失败");
        exit(1);
    }

    char buf[1024] = {0}; // 缓冲区
    size_t ret = 0;
    // 每次读1024字节,返回实际读取的字节数
    while ((ret = fread(buf, 1, 1024, pfin)) != 0) {
        fwrite(buf, 1, ret, pfout); // 写实际读取的字节数
    }

    // 判定结果...

    fclose(pfin);
    fclose(pfout);
}

10. ⚠️ 常见文件操作易错点(避坑指南)

  1. 未判断文件打开成功fopen返回NULL时直接操作,导致崩溃;
  2. 打开方式错误:只读模式("r")下写数据,或只写模式("w")下读数据;
  3. 忘记关闭文件:导致内存泄漏、缓冲区数据丢失;
  4. 关闭后未置空指针:文件指针成为野指针,后续误操作崩溃;
  5. char接收fgetc返回值:无法区分EOF(-1)和字符0xFF(255);
  6. fgetsnum参数过小:导致字符串被截断,未存储'\0'
  7. 二进制文件用记事本打开:看到乱码误以为写入失败(正常现象);
  8. 忽略缓冲区:未调用fflushfclose,数据未同步到硬盘。

11. ✅ 文件操作最佳实践(总结)

  1. 打开必判断fopen后必须检查返回值是否为NULL,用perror打印错误;
  2. 关闭必执行:读写完成后必须调用fclose,且关闭后将指针置空;
  3. 模式要匹配:读操作对应"r"/"rb",写操作对应"w"/"wb",追加对应"a"/"ab"
  4. 二进制优先:存储结构体、数组等复杂数据时,优先用二进制模式(高效、无转换);
  5. 缓冲区注意:需要立即同步数据时,调用fflush(如日志输出);
  6. 结束必判定:用feofferror区分 “文件尾” 和 “读错误”;
  7. 路径要正确:文件路径若包含空格或特殊字符,需用引号括起来(如"C:\my file.txt")。

文件操作是 C 语言的核心实用技能,掌握它能让你的程序具备数据持久化能力,无论是开发工具、日志系统还是数据处理程序,都离不开文件操作。如果这篇博客帮到了你,欢迎点赞收藏🌟~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值