文件操作
目录
文件操作
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; }易错点:
fgets的num必须大于字符串长度 + 1(留'\0'),否则会截断字符串;fgets会读取'\n'并存储在字符串中,打印时无需额外加'\n'。
5.3 格式化读写:fprintf/fscanf(结构体 / 多类型数据)
fprintf:格式化输出函数(写多类型数据)
- 原型:
int fprintf(FILE* stream, const char* format, ...); - 功能:将多类型数据(int/char/struct 等)按格式写入
stream流; - 对比
printf:printf默认写入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流按格式读取多类型数据; - 对比
scanf:scanf默认读取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:区分 “文件尾” 和 “读错误”)
文件读取结束有两种原因:
- 正常结束:读到文件末尾(
EOF); - 异常结束:读取过程中发生错误(如文件损坏、权限不足)。
仅通过fgetc返回EOF无法区分这两种情况,需要用feof和ferror函数判定。
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 语言文件操作存在 “缓冲区” 机制 —— 数据不会直接写入硬盘,而是先存入内存中的缓冲区,当缓冲区满、调用fflush或fclose时,才会将数据同步到硬盘。
缓冲区的作用:
- 减少硬盘 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(目标文件),支持文本和二进制文件。
核心思路:
- 打开源文件(读模式:
"r"或"rb")和目标文件(写模式:"w"或"wb"); - 循环读取源文件数据,写入目标文件;
- 关闭两个文件。
代码实现:
#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. ⚠️ 常见文件操作易错点(避坑指南)
- 未判断文件打开成功:
fopen返回NULL时直接操作,导致崩溃; - 打开方式错误:只读模式(
"r")下写数据,或只写模式("w")下读数据; - 忘记关闭文件:导致内存泄漏、缓冲区数据丢失;
- 关闭后未置空指针:文件指针成为野指针,后续误操作崩溃;
- 用
char接收fgetc返回值:无法区分EOF(-1)和字符0xFF(255); fgets的num参数过小:导致字符串被截断,未存储'\0';- 二进制文件用记事本打开:看到乱码误以为写入失败(正常现象);
- 忽略缓冲区:未调用
fflush或fclose,数据未同步到硬盘。
11. ✅ 文件操作最佳实践(总结)
- 打开必判断:
fopen后必须检查返回值是否为NULL,用perror打印错误; - 关闭必执行:读写完成后必须调用
fclose,且关闭后将指针置空; - 模式要匹配:读操作对应
"r"/"rb",写操作对应"w"/"wb",追加对应"a"/"ab"; - 二进制优先:存储结构体、数组等复杂数据时,优先用二进制模式(高效、无转换);
- 缓冲区注意:需要立即同步数据时,调用
fflush(如日志输出); - 结束必判定:用
feof和ferror区分 “文件尾” 和 “读错误”; - 路径要正确:文件路径若包含空格或特殊字符,需用引号括起来(如
"C:\my file.txt")。
文件操作是 C 语言的核心实用技能,掌握它能让你的程序具备数据持久化能力,无论是开发工具、日志系统还是数据处理程序,都离不开文件操作。如果这篇博客帮到了你,欢迎点赞收藏🌟~