文件操作
目录
1. 🌟 为什么需要文件操作?(内存数据的 “永久存储” 方案)
4. 🚪 文件的打开与关闭(核心操作:fopen/fclose)
5.3 格式化读写:fprintf/fscanf(结构体 / 多类型数据)
6. 🔀 文件的随机读写(灵活定位:fseek/ftell/rewind)
7. ❗ 文件读取结果判定(feof/ferror:区分 “文件尾” 和 “读错误”)
✨引言:
在 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 语言的核心实用技能,掌握它能让你的程序具备数据持久化能力,无论是开发工具、日志系统还是数据处理程序,都离不开文件操作。如果这篇博客帮到了你,欢迎点赞收藏🌟~
1404

被折叠的 条评论
为什么被折叠?



