11、文件复制实战:使用C语言实现文件复制功能(逐字符、逐行、逐块三种方式)
文件复制,听起来是不是很简单?
不就是把一个文件的内容,原封不动地搬到另一个文件里嘛。但真到了实际项目中,你会发现——复制方式选不对,性能差十倍甚至百倍。
我个人习惯把文件复制分成三个层次:逐字符、逐行、逐块。这三种方式各有各的适用场景,今天我就带你一个一个过一遍。
为什么需要三种复制方式?
你想想看,如果让你搬一箱水,你会一瓶一瓶拿,还是一次搬一箱?
逐字符复制,就像一瓶一瓶拿——最灵活,但最慢。逐行复制,就像一箱一箱搬——适合文本文件。逐块复制,就像用叉车一次搬一托盘——适合大文件,效率最高。
我在项目中遇到过这样一个坑:用逐字符方式复制一个 500MB 的日志文件,程序跑了快 10 分钟还没结束。后来改成逐块复制,不到 3 秒就完成了。嗯,选对方式真的很重要。
逐字符复制:最基础的方式
逐字符复制,说白了就是一次读一个字符,一次写一个字符。代码逻辑非常直观:
#include <stdio.h>
int copy_char_by_char(const char *src, const char *dst) {
FILE *fp_src = fopen(src, "r");
FILE *fp_dst = fopen(dst, "w");
if (fp_src == NULL || fp_dst == NULL) {
printf("文件打开失败\n");
return -1;
}
int ch; // 注意:用 int 而不是 char
while ((ch = fgetc(fp_src)) != EOF) {
fputc(ch, fp_dst);
}
fclose(fp_src);
fclose(fp_dst);
return 0;
}
这里有个细节我要提醒你:ch 的类型必须是 int,不能是 char。为什么?因为 EOF 的值是 -1,而 char 类型在某些平台上是无符号的,永远无法等于 -1。我曾经因为这个 bug 排查了整整一个下午……
逐行复制:文本文件的专属方案
如果你要复制的是文本文件(比如 .txt、.csv、.log),逐行复制是个不错的选择。它一次处理一行,代码写起来也最自然:
#include <stdio.h>
#include <string.h>
int copy_line_by_line(const char *src, const char *dst) {
FILE *fp_src = fopen(src, "r");
FILE *fp_dst = fopen(dst, "w");
if (fp_src == NULL || fp_dst == NULL) {
printf("文件打开失败\n");
return -1;
}
char buffer[1024];
while (fgets(buffer, sizeof(buffer), fp_src) != NULL) {
fputs(buffer, fp_dst);
}
fclose(fp_src);
fclose(fp_dst);
return 0;
}
这里有个问题:fgets 需要指定缓冲区大小。如果一行超过 1024 字节,它会分多次读取。这其实不是 bug,而是特性——它保证了不会缓冲区溢出。
我个人习惯把缓冲区设成 4096 或 8192,这样大部分文本文件的一行都能一次读完。
getline() 函数(POSIX 标准),它会自动分配足够大的缓冲区。
逐块复制:大文件的终极方案
好了,重点来了。逐块复制是效率最高的方式,也是我在实际项目中最常用的。它的核心思想是:一次读一大块数据到内存,然后一次写出去。
#include <stdio.h>
int copy_block_by_block(const char *src, const char *dst) {
FILE *fp_src = fopen(src, "rb");
FILE *fp_dst = fopen(dst, "wb");
if (fp_src == NULL || fp_dst == NULL) {
printf("文件打开失败\n");
return -1;
}
char buffer[8192]; // 8KB 缓冲区
size_t bytes_read;
while ((bytes_read = fread(buffer, 1, sizeof(buffer), fp_src)) > 0) {
fwrite(buffer, 1, bytes_read, fp_dst);
}
fclose(fp_src);
fclose(fp_dst);
return 0;
}
注意看,这里我用了 "rb" 和 "wb" 模式——二进制模式。为什么?因为逐块复制通常用于二进制文件(图片、视频、压缩包等),而且二进制模式不会对换行符做转换,保证了数据的完整性。
缓冲区大小怎么选?我做过测试:
| 缓冲区大小 | 复制 100MB 文件耗时 | 说明 |
|---|---|---|
| 1KB | 约 2.3 秒 | 系统调用次数太多 |
| 8KB | 约 0.4 秒 | 推荐大小,性能与内存的平衡点 |
| 64KB | 约 0.3 秒 | 性能更好,但内存占用稍高 |
| 1MB | 约 0.28 秒 | 提升不明显,内存占用大 |
从表格可以看出,8KB 到 64KB 是最佳区间。再大,性能提升就微乎其微了。
三种方式的对比总结
为了方便你选择,我整理了一张对比表:
| 方式 | 适用场景 | 效率 | 代码复杂度 |
|---|---|---|---|
| 逐字符 | 小文本文件、需要逐字符处理 | 极低 | 低 |
| 逐行 | 文本文件、日志文件 | 中等 | 低 |
| 逐块 | 大文件、二进制文件 | 极高 | 中 |
知识体系图
下面这张图,帮你理清这三种复制方式的关系和选择逻辑:
避坑指南
最后,分享几个我踩过的坑:
- 文件打开模式要匹配:文本模式("r"/"w")和二进制模式("rb"/"wb")不能混用。我曾经用文本模式复制一个 PNG 图片,结果图片损坏了——因为 0x1A 被当成了 EOF 处理。
- 别忘了检查返回值:
fread和fwrite的返回值一定要检查。如果写入的字节数不等于读取的字节数,说明磁盘空间不足或发生了错误。 - 缓冲区大小不是越大越好:超过 1MB 的缓冲区,性能提升微乎其微,反而浪费内存。8KB 到 64KB 是最佳选择。
- 逐行复制时注意行长度:如果一行超过缓冲区大小,
fgets会分多次读取。这时候你的逻辑要能正确处理跨缓冲区的行。
好了,文件复制的三种方式就讲到这里。记住:没有最好的方式,只有最合适的方式。根据你的文件大小和类型,选择对应的复制策略,才能写出高效、健壮的代码。