12、文件合并实战:将多个文件内容合并到一个文件中
文件合并,说白了就是把几个零散的文件拼成一个完整的文件。这在实际开发中太常见了——比如日志合并、配置拼接、数据汇总。我当年刚入行时,第一个正式任务就是写一个日志合并工具,那时候还不太懂缓冲区的概念,结果写出来的程序慢得像蜗牛爬。嗯,今天咱们就把这个坑填上。
12.1 合并的核心思路
合并文件的逻辑其实很简单:
- 打开目标文件(准备写入)
- 依次打开每个源文件(准备读取)
- 把源文件的内容复制到目标文件中
- 关闭所有文件
你想想看,这不就是「读一个文件,写一个文件」的循环吗?只不过循环的次数取决于你要合并多少个文件。
核心要点:合并操作的本质是「读-写」循环,而不是「读-存-写」。千万别把整个文件读到内存里再写,那样大文件会直接撑爆内存。
12.2 基础实现:逐个文件合并
先看一个最直接的实现。我习惯用 fread 和 fwrite 配合缓冲区来做,效率比逐字符读写高得多。
#include <stdio.h>
#include <stdlib.h>
#define BUFFER_SIZE 4096
int merge_files(const char *output, const char *files[], int count) {
FILE *fp_out = fopen(output, "wb");
if (!fp_out) {
perror("无法打开目标文件");
return -1;
}
char buffer[BUFFER_SIZE];
size_t bytes_read;
for (int i = 0; i < count; i++) {
FILE *fp_in = fopen(files[i], "rb");
if (!fp_in) {
fprintf(stderr, "无法打开源文件: %s\n", files[i]);
continue; // 跳过失败的文件,继续合并
}
while ((bytes_read = fread(buffer, 1, BUFFER_SIZE, fp_in)) > 0) {
fwrite(buffer, 1, bytes_read, fp_out);
}
fclose(fp_in);
printf("已合并: %s\n", files[i]);
}
fclose(fp_out);
return 0;
}
int main() {
const char *src_files[] = {"part1.txt", "part2.txt", "part3.txt"};
int count = sizeof(src_files) / sizeof(src_files[0]);
if (merge_files("merged.txt", src_files, count) == 0) {
printf("合并完成!\n");
}
return 0;
}
小技巧:缓冲区大小设为 4096 字节(4KB)是个不错的选择。太小了频繁调用系统IO,太大了浪费内存。4KB 是磁盘扇区的整数倍,性能比较均衡。
12.3 加个分隔符,让合并更清晰
直接合并有个问题——你分不清哪里是文件A的结尾,哪里是文件B的开头。我曾经在合并配置文件时没加分隔符,结果两个 JSON 文件拼在一起直接解析失败,排查了半天才发现问题。
解决方案很简单:在文件之间插入一个分隔标记。
void write_separator(FILE *fp, const char *filename) {
fprintf(fp, "\n/* === 文件分隔: %s === */\n\n", filename);
}
// 在 merge_files 函数的循环中调用:
// 写入源文件内容之前,先写入分隔符
write_separator(fp_out, files[i]);
这样合并后的文件里,每个源文件的内容都有明确的边界,不管是人看还是程序解析都方便多了。
12.4 处理大文件:别让内存成为瓶颈
如果文件特别大(比如几百MB甚至GB级别),上面的代码依然能工作,因为我们是分块读写的。但有几个细节要注意:
- 缓冲区大小可以调大:比如 64KB 或 256KB,减少循环次数
- 使用
setvbuf设置流缓冲区:让标准库自己管理缓冲 - 监控磁盘空间:合并前检查目标磁盘是否有足够空间
// 设置更大的流缓冲区
FILE *fp_out = fopen(output, "wb");
char *stream_buffer = malloc(65536); // 64KB
setvbuf(fp_out, stream_buffer, _IOFBF, 65536);
注意:用 setvbuf 时,缓冲区必须在文件关闭前一直有效。别在函数里用局部变量当缓冲区,函数返回后缓冲区就失效了,程序会崩溃。
12.5 错误处理:别让一个坏文件毁了全部
实际项目中,源文件可能损坏、被占用、或者路径不对。我建议这样处理:
- 记录失败的文件名,但继续合并其他文件
- 最后返回失败的文件数量,而不是直接退出
- 如果目标文件写入失败,立即停止并报错
int merge_files_robust(const char *output, const char *files[], int count) {
FILE *fp_out = fopen(output, "wb");
if (!fp_out) return -1;
int fail_count = 0;
char buffer[8192];
size_t n;
for (int i = 0; i < count; i++) {
FILE *fp_in = fopen(files[i], "rb");
if (!fp_in) {
fprintf(stderr, "警告: 跳过 %s\n", files[i]);
fail_count++;
continue;
}
while ((n = fread(buffer, 1, sizeof(buffer), fp_in)) > 0) {
if (fwrite(buffer, 1, n, fp_out) != n) {
perror("写入失败");
fclose(fp_in);
fclose(fp_out);
return -2; // 写入错误,严重问题
}
}
fclose(fp_in);
}
fclose(fp_out);
if (fail_count > 0) {
printf("合并完成,但有 %d 个文件失败\n", fail_count);
}
return fail_count;
}
12.6 知识体系图
下面这张图帮你理清文件合并的整体脉络:
12.7 实战建议
最后,给你几个我踩过坑之后总结的建议:
- 先测试小文件:用几个几十字节的文本文件测试逻辑,确认没问题再上大文件
- 注意文件编码:如果合并的是文本文件,确保所有源文件编码一致,否则会出现乱码
- 考虑文件顺序:合并的顺序就是文件列表的顺序,按需排序后再合并
- 加个进度提示:如果文件很大,每合并一个文件打印一条信息,让用户知道程序还在跑
我的习惯:写文件合并工具时,我会额外加一个 --dry-run 参数,只检查文件是否存在、磁盘空间是否足够,但不实际执行合并。这样可以在正式合并前发现潜在问题,避免写到一半报错。
文件合并看起来简单,但做好错误处理和性能优化,才能写出真正能用的工具。下次遇到需要拼接文件的需求,直接拿上面的代码改改就能用。
公众号:蓝海资料掘金营,微信deep3321