12、文件合并实战:将多个文件内容合并到一个文件中

文件合并,说白了就是把几个零散的文件拼成一个完整的文件。这在实际开发中太常见了——比如日志合并、配置拼接、数据汇总。我当年刚入行时,第一个正式任务就是写一个日志合并工具,那时候还不太懂缓冲区的概念,结果写出来的程序慢得像蜗牛爬。嗯,今天咱们就把这个坑填上。

12.1 合并的核心思路

合并文件的逻辑其实很简单:

  1. 打开目标文件(准备写入)
  2. 依次打开每个源文件(准备读取)
  3. 把源文件的内容复制到目标文件中
  4. 关闭所有文件

你想想看,这不就是「读一个文件,写一个文件」的循环吗?只不过循环的次数取决于你要合并多少个文件。

核心要点:合并操作的本质是「读-写」循环,而不是「读-存-写」。千万别把整个文件读到内存里再写,那样大文件会直接撑爆内存。

12.2 基础实现:逐个文件合并

先看一个最直接的实现。我习惯用 freadfwrite 配合缓冲区来做,效率比逐字符读写高得多。

#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 知识体系图

下面这张图帮你理清文件合并的整体脉络:

文件合并实战知识体系 源文件 1 源文件 2 源文件 3 ...更多文件 合并核心 fread + fwrite 缓冲区循环 合并结果 merged.txt 关键要点 • 分块读写,避免内存爆炸 • 加分隔符,便于识别文件边界 • 错误容忍,单个文件失败不影响整体

12.7 实战建议

最后,给你几个我踩过坑之后总结的建议:

  • 先测试小文件:用几个几十字节的文本文件测试逻辑,确认没问题再上大文件
  • 注意文件编码:如果合并的是文本文件,确保所有源文件编码一致,否则会出现乱码
  • 考虑文件顺序:合并的顺序就是文件列表的顺序,按需排序后再合并
  • 加个进度提示:如果文件很大,每合并一个文件打印一条信息,让用户知道程序还在跑

我的习惯:写文件合并工具时,我会额外加一个 --dry-run 参数,只检查文件是否存在、磁盘空间是否足够,但不实际执行合并。这样可以在正式合并前发现潜在问题,避免写到一半报错。

文件合并看起来简单,但做好错误处理和性能优化,才能写出真正能用的工具。下次遇到需要拼接文件的需求,直接拿上面的代码改改就能用。


公众号:蓝海资料掘金营,微信deep3321