第一章:C 语言文件操作基础概念
1.1 文件是什么?
在 C 语言中,文件是存储在外部介质(如硬盘)上的一系列数据的集合。可以是文本文件(如.txt
,人类可读)或二进制文件(如.exe
、图片、音频,计算机直接读取)。
核心特点:文件操作需要通过 “文件指针”(FILE *
)作为 “中间人”,告诉程序 “当前操作哪个文件”。
1.2 文件打开与关闭:一切操作的起点和终点
-
打开文件:
fopen()
函数
格式:FILE *fopen(const char *filename, const char *mode);
filename
:文件路径(如"D:\\test.txt"
或"./data.bin"
)。mode
:打开模式(必须记牢!):模式 含义 文本文件 / 二进制文件 r
只读(文件必须存在) 文本 w
写入(不存在则创建,存在则清空) 文本 a
追加(不存在则创建,存在则在末尾添加) 文本 rb
只读(二进制模式) 二进制 wb
写入(二进制模式) 二进制 ab
追加(二进制模式) 二进制 r+
读写(文件必须存在) 文本 w+
读写(创建或清空) 文本 a+
读写(追加,不清空) 文本 rb+
读写(二进制模式) 二进制 重点:复制文件时,源文件用 r
或rb
,目标文件用w
或wb
(若需保留目标文件原有内容并追加,用a
,但复制通常是全新覆盖,所以用w
)。
错误处理:必须检查
fopen
是否返回NULL
(文件打开失败,比如路径错误、无权限):FILE *src_file = fopen("source.txt", "r"); if (src_file == NULL) { printf("无法打开源文件!\n"); exit(1); // 终止程序 }
-
关闭文件:
fclose()
函数
格式:int fclose(FILE *stream);
- 必须关闭!否则可能导致数据未写入磁盘(缓冲区未刷新)或资源泄漏。
- 返回值:成功返回
0
,失败返回EOF
(需用ferror()
进一步检查错误)。
第二章:文件复制的核心逻辑:“读 - 写” 循环
文件复制的本质是一个循环过程:
从源文件读取数据 → 写入目标文件 → 重复直到源文件结束。
根据读取单位的不同,分为 “字符级复制” 和 “块级复制”,下面详细讲解。
2.1 字符级复制:适合文本文件(逐个字符搬运)
使用fgetc()
(读字符)和fputc()
(写字符),每次处理一个字节(文本文件中一个字符通常对应一个字节,如 ASCII 字符)。
步骤示例:
- 打开源文件(读模式)和目标文件(写模式)。
- 用
fgetc
从源文件读一个字符,直到遇到EOF
(文件结束标志)。 - 用
fputc
将字符写入目标文件。 - 关闭两个文件。
代码模板:
#include <stdio.h>
#include <stdlib.h> // 用于exit()
void copy_file_char(const char *src_path, const char *dst_path) {
FILE *src = fopen(src_path, "r");
FILE *dst = fopen(dst_path, "w");
if (src == NULL || dst == NULL) {
printf("文件打开失败!\n");
if (src) fclose(src); // 确保失败时关闭已打开的文件
if (dst) fclose(dst);
exit(1);
}
int ch; // 用int存储,因为fgetc返回值可能是EOF(-1),char无法表示
while ((ch = fgetc(src)) != EOF) { // 循环读取直到文件结束
fputc(ch, dst); // 写入目标文件
}
fclose(src);
fclose(dst);
printf("字符级复制完成!\n");
}
int main() {
copy_file_char("source.txt", "destination.txt");
return 0;
}
注意事项:
- 文本文件中,
fgetc
会自动处理换行符(如 Windows 的\r\n
在读取时转为\n
,写入时反之),但二进制文件不能用这种方式,因为二进制数据中可能包含EOF
对应的数值(-1),导致提前终止。 - 效率问题:每次读写一个字符,会频繁调用操作系统接口,效率低,不适合大文件(比如 1GB 的文件需要 10 亿次调用)。
2.2 块级复制:适合所有文件(批量搬运,效率更高)
使用fread()
(读块)和fwrite()
(写块),每次读取一个缓冲区大小的数据(如 1KB、4KB),减少系统调用次数。
核心函数:
size_t fread(void *ptr, size_t size, size_t count, FILE *stream);
- 从
stream
读取count
个大小为size
的块,存入ptr
指向的缓冲区,返回实际读取的块数。
- 从
size_t fwrite(const void *ptr, size_t size, size_t count, FILE *stream);
- 向
stream
写入count
个大小为size
的块,从ptr
读取数据,返回实际写入的块数。
- 向
步骤示例:
- 打开源文件(二进制读模式
rb
)和目标文件(二进制写模式wb
)——二进制模式是关键,避免系统自动转换字符(如换行符),确保数据原样复制。 - 创建缓冲区(如
char buffer[4096];
,通常用 4KB 或 8KB,匹配磁盘块大小)。 - 循环:
- 从源文件读
buffer
大小的数据到缓冲区。 - 将缓冲区数据写入目标文件。
- 直到读取的字节数为 0(文件结束)。
- 从源文件读
- 关闭文件。
代码模板:
#include <stdio.h>
#include <stdlib.h>
#define BUFFER_SIZE 4096 // 缓冲区大小,可调整(通常为4KB的倍数)
void copy_file_block(const char *src_path, const char *dst_path) {
FILE *src = fopen(src_path, "rb"); // 二进制读
FILE *dst = fopen(dst_path, "wb"); // 二进制写
if (src == NULL || dst == NULL) {
printf("文件打开失败!\n");
if (src) fclose(src);
if (dst) fclose(dst);
exit(1);
}
char buffer[BUFFER_SIZE];
size_t bytes_read; // 记录实际读取的字节数
while ((bytes_read = fread(buffer, 1, BUFFER_SIZE, src)) > 0) { // 每次读BUFFER_SIZE字节
fwrite(buffer, 1, bytes_read, dst); // 写入刚读取的字节数(最后一次可能不足BUFFER_SIZE)
}
fclose(src);
fclose(dst);
printf("块级复制完成!\n");
}
int main() {
copy_file_block("source.bin", "destination.bin");
return 0;
}
优势:
- 适用于所有文件类型(文本、二进制),因为二进制模式下不会误解数据(比如不会把
0x0A
和0x0D
当作换行符转换)。 - 效率高:假设缓冲区是 4KB,1GB 文件只需约 262,144 次读写,比字符级的 10 亿次快太多。
第三章:深入理解文件复制的关键细节
3.1 文本文件 vs 二进制文件:复制的区别
-
文本文件:
- 人类可读,由字符组成(如 ASCII、UTF-8)。
- 在不同系统中换行符不同(Windows:
\r\n
,Linux:\n
,Mac:\r
)。 - 使用
fgetc
/fputc
时,C 语言会自动转换换行符(比如读 Windows 文件时,\r\n
会被转为\n
存储;写入时,\n
会转为\r\n
)。 - 注意:如果文本文件包含非文本数据(如二进制内容),可能导致错误(比如
fgetc
遇到EOF
值会提前终止),所以复制文本文件也推荐用二进制模式,避免换行符转换干扰。
-
二进制文件:
- 直接存储二进制数据(如图片、可执行文件),不能用文本编辑器正确查看。
- 必须用二进制模式(
rb
/wb
)打开,避免系统自动转换字符,确保每个字节都被原样复制。 fread
/fwrite
按字节块操作,不会误解数据中的EOF
(因为判断依据是读取的字节数是否为 0,而非某个特定值)。
结论:通用的文件复制应使用二进制模式和块级复制,既能处理所有文件类型,又能保证效率。
3.2 缓冲区的作用:为什么不能没有它?
- 缓冲区是内存中的一块区域,用于暂存数据。
- 没有缓冲区时(比如每次读写 1 字节):
- 每次
fgetc
/fputc
都要调用操作系统的底层文件操作,涉及用户态和内核态的切换,开销极大。
- 每次
- 有缓冲区时:
fread
先将数据批量读入内存缓冲区,填满后再一次性写入目标文件的缓冲区,最后由操作系统批量写入磁盘。- 减少系统调用次数,提升效率(内存访问比磁盘 IO 快百万倍)。
缓冲区大小如何选?
- 通常设为 4096 字节(4KB),因为这是多数磁盘的块大小,与底层存储单元匹配,效率最佳。
- 过大的缓冲区(如 1MB)可能占用过多内存,尤其是同时复制多个文件时;过小则效率不高。
3.3 文件结束的判断:EOF
vs 读取字节数为 0
-
文本文件(字符级复制):用
fgetc
返回EOF
(-1)判断结束,但需注意:- 如果文件包含值为
0xFF
(二进制)的字节,在文本模式下可能被误判为EOF
(因为EOF
通常定义为 - 1,即二进制补码全 1)。 - 所以文本文件也建议用二进制模式和块级复制,通过
fread
的返回值判断是否结束(返回 0 时结束)。
- 如果文件包含值为
-
二进制文件(块级复制):直接检查
fread
的返回值,当读取的字节数为 0 时,说明文件结束,不会被数据中的特定值干扰。
3.4 错误处理:比代码本身更重要
文件操作中可能遇到的错误包括:
- 源文件不存在(
fopen
返回NULL
)。 - 目标文件无法创建(如目录不存在、无写入权限)。
- 读取或写入过程中磁盘空间不足。
- 文件被其他程序占用(无法打开)。
完善的错误处理代码示例:
void copy_file_safe(const char *src_path, const char *dst_path) {
FILE *src = fopen(src_path, "rb");
if (src == NULL) {
perror("打开源文件失败"); // 打印具体错误信息(如“没有那个文件或目录”)
exit(1);
}
FILE *dst = fopen(dst_path, "wb");
if (dst == NULL) {
perror("创建目标文件失败");
fclose(src);
exit(1);
}
char buffer[BUFFER_SIZE];
size_t bytes_read;
while (1) {
bytes_read = fread(buffer, 1, BUFFER_SIZE, src);
if (bytes_read == 0) { // 正常结束或错误
if (ferror(src)) { // 检查读取时是否出错
perror("读取源文件时发生错误");
break;
}
break; // 正常结束
}
if (fwrite(buffer, 1, bytes_read, dst) != bytes_read) { // 写入字节数不等于读取字节数,说明写入错误
perror("写入目标文件时发生错误");
break;
}
}
fclose(src);
fclose(dst);
// 可根据是否发生错误输出不同信息
}
关键函数:
perror(char *s)
:打印上次错误的具体信息(如 “权限被拒绝”“磁盘空间不足”),需包含stdio.h
。ferror(FILE *stream)
:返回非零值表示流上发生了错误,需手动检查(因为fread
/fwrite
返回 0 可能是正常结束,也可能是错误)。
第四章:扩展应用与最佳实践
4.1 命令行参数实现文件复制(类似cp
命令)
让程序支持通过命令行输入源文件和目标文件路径,如:
./copy.exe source.txt destination.txt
代码实现:
#include <stdio.h>
#include <stdlib.h>
#define BUFFER_SIZE 4096
int main(int argc, char *argv[]) {
if (argc != 3) { // 检查参数数量是否正确(程序名+源文件+目标文件)
printf("用法:%s 源文件 目标文件\n", argv[0]);
return 1;
}
const char *src_path = argv[1];
const char *dst_path = argv[2];
FILE *src = fopen(src_path, "rb");
if (src == NULL) {
perror("打开源文件失败");
return 1;
}
FILE *dst = fopen(dst_path, "wb");
if (dst == NULL) {
perror("创建目标文件失败");
fclose(src);
return 1;
}
char buffer[BUFFER_SIZE];
size_t bytes_read;
while ((bytes_read = fread(buffer, 1, BUFFER_SIZE, src)) > 0) {
if (fwrite(buffer, 1, bytes_read, dst) != bytes_read) {
perror("写入目标文件时发生错误");
break;
}
}
fclose(src);
fclose(dst);
printf("文件复制成功!\n");
return 0;
}
注意:
argc
是命令行参数个数,argv
是参数数组,argv[0]
是程序名本身。- 这种写法更接近 Linux 的
cp
命令,方便用户在终端使用。
4.2 复制文件夹:文件复制的 “升级版”
C 语言标准库没有直接复制文件夹的函数,因为文件夹涉及递归遍历子文件和子目录。需借助:
opendir()
/readdir()
遍历目录中的文件。mkdir()
创建目标文件夹。- 对每个文件调用文件复制函数。
伪代码逻辑:
void copy_dir(const char *src_dir, const char *dst_dir) {
// 创建目标目录
mkdir(dst_dir);
// 打开源目录
DIR *dir = opendir(src_dir);
struct dirent *entry;
while ((entry = readdir(dir)) != NULL) {
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {
continue; // 跳过当前目录和父目录
}
char src_path[1024], dst_path[1024];
sprintf(src_path, "%s/%s", src_dir, entry->d_name);
sprintf(dst_path, "%s/%s", dst_dir, entry->d_name);
struct stat statbuf;
stat(src_path, &statbuf);
if (S_ISDIR(statbuf.st_mode)) { // 如果是子目录,递归复制
copy_dir(src_path, dst_path);
} else { // 如果是文件,调用文件复制函数
copy_file_block(src_path, dst_path);
}
}
closedir(dir);
}
需要的头文件:
#include <dirent.h>
(目录操作)#include <sys/stat.h>
(获取文件属性)- 注意:此代码为 Linux/macOS 版本,Windows 需用
Windows API
(如CreateFile
、FindFirstFile
)实现,跨平台需额外处理。
4.3 大文件复制:性能优化关键点
- 缓冲区大小:设为 4KB、8KB 或更大(如 1MB),但需注意内存占用(64 位系统可适当增大)。
- 多线程 / 异步 IO:C 语言标准库不支持多线程文件复制,但可通过操作系统 API(如 Linux 的
pthreads
)实现,但需注意文件指针的线程安全(每个线程操作独立的文件指针)。 - 避免频繁打开 / 关闭文件:一次性打开源文件和目标文件,完成整个复制流程后再关闭。
第五章:常见错误与解决方案
错误现象 | 可能原因 | 解决方案 |
---|---|---|
目标文件为空 | 源文件打开失败,未进入读写循环 | 检查fopen 是否返回NULL ,添加错误处理 |
复制后文件内容错误 | 使用文本模式复制二进制文件 | 改用二进制模式(rb /wb ) |
程序崩溃 | 未检查fgetc 返回值直接转char | 用int 存储fgetc 结果,避免溢出 |
复制速度极慢 | 使用字符级复制且缓冲区过小 | 改用块级复制,增大缓冲区到 4KB 以上 |
目标文件无法删除 / 修改 | 未正确关闭文件(文件被占用) | 确保fclose 被调用,检查是否有异常分支遗漏 |
第六章:总结与学习建议
-
核心流程:
打开文件 → 读取数据 → 写入数据 → 关闭文件,其中 “二进制模式 + 块级复制” 是通用方案。 -
必背函数:
fopen()
/fclose()
:文件生命周期管理。fread()
/fwrite()
:高效读写二进制数据。perror()
/ferror()
:错误处理必备。
形象比喻:把文件复制想象成 “搬砖游戏”
你可以把 “文件复制” 想象成一个超级简单的 “搬砖游戏”:
- 两个房子(文件):你有一个 “源文件”(比如装满砖块的老房子)和一个 “目标文件”(空的新房子)。
- 卡车(文件指针):你需要开着两辆卡车,分别通向老房子和新房子,告诉电脑 “我要操作这两个文件了”。
- 搬砖工(读写函数):卡车司机不能直接搬砖,需要搬砖工帮忙。每次搬一块砖(字符)或者一堆砖(缓冲区),从老房子搬到新房子。
- 关门(关闭文件):搬完砖后,一定要记得把两辆卡车开走,关紧房门,否则可能丢砖(数据丢失或占用资源)。
一句话总结流程:
打开源文件(老房子)→ 打开目标文件(新房子)→ 一块一块(或一堆一堆)把数据从源文件搬到目标文件→ 关闭两个文件(关门)。