实战项目3:实现一个文件差异比较工具,支持文本与二进制模式

说实话,文件差异比较这个需求,我在工作中碰到过太多次了。

有一次,两个固件版本烧出来行为不一样,我得找出到底是哪几个字节变了。还有一次,同事改了一版配置文件,结果系统跑不起来,我得一行一行看改了啥。那时候我就想,干脆自己写个 diff 工具算了。

今天这个项目,我们就来手撸一个。它支持两种模式:文本模式(逐行比较)和二进制模式(逐字节比较)。

项目需求与设计思路

先说说我们要做什么。

  • 输入两个文件名,以及一个模式参数("text" 或 "binary")。
  • 文本模式下,按行读取,逐行比较,输出差异的行号与内容。
  • 二进制模式下,按块读取(比如 512 字节一块),逐字节比较,输出第一个不同的偏移地址和字节值。
  • 如果文件完全一致,输出 "Files are identical."。

嗯,这里要注意:文本模式不能简单用 fgets 一行一行比,因为行尾可能有换行符差异(Windows 的 \r\n vs Linux 的 \n)。我个人习惯在比较前先去掉行尾空白,或者统一转换成同一种格式。

二进制模式就简单多了,直接读内存块,用 memcmp 或者自己逐字节比。但如果你要输出所有不同的位置,那就得自己写循环了。

核心设计图

文件差异比较工具 — 整体流程 输入两个文件名 + 模式 模式判断 文本模式:逐行比较 二进制模式:逐块比较 输出差异行号/偏移地址,或 "Files are identical."

文本模式的核心实现

文本模式说白了就是逐行扫描。我习惯用 fgets 读一行,然后去掉末尾的换行符,再跟另一文件的对应行做比较。

但有个坑:两个文件行数可能不一样。比如文件 A 有 100 行,文件 B 只有 80 行。那从第 81 行开始,A 的所有行都算新增内容。

小技巧: 我一般用 strcspn 来去掉换行符,比手动写循环快多了。像这样:line[strcspn(line, "\n")] = '\0';

// 文本模式比较核心代码
void diff_text(FILE *fp1, FILE *fp2) {
    char line1[1024], line2[1024];
    int line_num = 1;
    int has_diff = 0;

    while (1) {
        char *r1 = fgets(line1, sizeof(line1), fp1);
        char *r2 = fgets(line2, sizeof(line2), fp2);

        if (r1 == NULL && r2 == NULL) break;

        // 去掉行尾换行符
        if (r1) line1[strcspn(line1, "\n")] = '\0';
        if (r2) line2[strcspn(line2, "\n")] = '\0';

        if ((r1 == NULL) || (r2 == NULL) || (strcmp(line1, line2) != 0)) {
            printf("差异行 %d:\n", line_num);
            if (r1) printf("  < %s\n", line1);
            else    printf("  < (文件结束)\n");
            if (r2) printf("  > %s\n", line2);
            else    printf("  > (文件结束)\n");
            has_diff = 1;
        }
        line_num++;
    }

    if (!has_diff) printf("Files are identical.\n");
}

你看,逻辑其实不复杂。但我在项目中遇到过一个问题:如果某一行特别长(比如超过 1024 字节),fgets 会截断它。这时候比较就会出错。所以我建议,如果处理大文件,可以用动态分配缓冲区,或者用 getline(POSIX 标准)。

二进制模式的核心实现

二进制模式就粗暴多了。我直接按块读,每块 512 字节,然后用 memcmp 快速判断。如果某一块不同,再逐字节定位。

为什么先整块比较?因为 memcmp 是内存级别的比较,速度极快。你想想看,如果两个文件有 100MB,逐字节比要 1 亿次循环,而用 memcmp 配合大块读取,可能只需要几百次比较。

注意: 二进制模式下,不要用 fgetsfscanf,它们遇到 \0 会停止。一定要用 fread

// 二进制模式核心代码
void diff_binary(FILE *fp1, FILE *fp2) {
    unsigned char buf1[512], buf2[512];
    size_t offset = 0;
    int has_diff = 0;

    while (1) {
        size_t n1 = fread(buf1, 1, sizeof(buf1), fp1);
        size_t n2 = fread(buf2, 1, sizeof(buf2), fp2);

        if (n1 == 0 && n2 == 0) break;

        // 先快速比较整块
        size_t min_n = (n1 < n2) ? n1 : n2;
        if (memcmp(buf1, buf2, min_n) != 0 || n1 != n2) {
            // 逐字节定位
            for (size_t i = 0; i < min_n; i++) {
                if (buf1[i] != buf2[i]) {
                    printf("偏移 0x%08zX: 文件1=0x%02X 文件2=0x%02X\n",
                           offset + i, buf1[i], buf2[i]);
                    has_diff = 1;
                }
            }
            // 处理长度不一致的情况
            if (n1 > n2) {
                for (size_t i = n2; i < n1; i++)
                    printf("偏移 0x%08zX: 文件1=0x%02X 文件2=(文件结束)\n",
                           offset + i, buf1[i]);
                has_diff = 1;
            } else if (n2 > n1) {
                for (size_t i = n1; i < n2; i++)
                    printf("偏移 0x%08zX: 文件1=(文件结束) 文件2=0x%02X\n",
                           offset + i, buf2[i]);
                has_diff = 1;
            }
        }
        offset += min_n;
    }

    if (!has_diff) printf("Files are identical.\n");
}

嗯,这里有个细节:偏移地址用 %08zX 格式化,z 表示 size_t 类型,08 表示至少输出 8 位十六进制数,前面补零。这样输出对齐,看着舒服。

主函数与模式选择

主函数就简单了,解析命令行参数,打开文件,根据模式调用对应的函数。

int main(int argc, char *argv[]) {
    if (argc != 4) {
        fprintf(stderr, "用法: %s <文件1> <文件2> <模式:text|binary>\n", argv[0]);
        return 1;
    }

    FILE *fp1 = fopen(argv[1], "rb");
    FILE *fp2 = fopen(argv[2], "rb");
    if (!fp1 || !fp2) {
        perror("打开文件失败");
        return 1;
    }

    if (strcmp(argv[3], "text") == 0) {
        diff_text(fp1, fp2);
    } else if (strcmp(argv[3], "binary") == 0) {
        diff_binary(fp1, fp2);
    } else {
        fprintf(stderr, "未知模式: %s (请使用 text 或 binary)\n", argv[3]);
    }

    fclose(fp1);
    fclose(fp2);
    return 0;
}

注意,我打开文件时用了 "rb" 模式。为什么?因为在 Windows 下,"r" 模式会把 \r\n 转换成 \n,这对二进制比较是灾难。用 "rb" 可以保证原样读取。文本模式下其实也可以用 "rb",然后自己处理换行符,这样跨平台更安全。

避坑指南与经验总结

我曾经在做一个嵌入式项目时,需要比较两个固件镜像的差异。当时偷懒,直接用 fgets 读二进制文件,结果遇到 \0 就停了,后面全没读到。那次排查了整整一个下午才找到原因。所以,二进制文件一定要用 fread,这是铁律。

另外,文本模式下还有一个常见问题:编码差异。比如一个文件是 UTF-8,另一个是 GBK,那逐行比较肯定全是差异。我一般会在比较前先统一编码,或者至少给用户一个提示。

总结一下这个项目的关键点:

  • 文本模式用 fgets + strcmp,注意处理行尾和行数不一致。
  • 二进制模式用 fread + memcmp,先整块比较再逐字节定位,效率高。
  • 文件打开统一用 "rb",避免平台差异。
  • 偏移地址用 %08zX 格式化,输出整齐。

这个工具虽然简单,但非常实用。你把它编译出来,放到 /usr/local/bin 下,以后随时可以用。我个人电脑上就一直留着这个工具,比系统自带的 diff 轻量多了。


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