实战项目3:实现一个文件差异比较工具,支持文本与二进制模式
说实话,文件差异比较这个需求,我在工作中碰到过太多次了。
有一次,两个固件版本烧出来行为不一样,我得找出到底是哪几个字节变了。还有一次,同事改了一版配置文件,结果系统跑不起来,我得一行一行看改了啥。那时候我就想,干脆自己写个 diff 工具算了。
今天这个项目,我们就来手撸一个。它支持两种模式:文本模式(逐行比较)和二进制模式(逐字节比较)。
项目需求与设计思路
先说说我们要做什么。
- 输入两个文件名,以及一个模式参数("text" 或 "binary")。
- 文本模式下,按行读取,逐行比较,输出差异的行号与内容。
- 二进制模式下,按块读取(比如 512 字节一块),逐字节比较,输出第一个不同的偏移地址和字节值。
- 如果文件完全一致,输出 "Files are identical."。
嗯,这里要注意:文本模式不能简单用 fgets 一行一行比,因为行尾可能有换行符差异(Windows 的 \r\n vs Linux 的 \n)。我个人习惯在比较前先去掉行尾空白,或者统一转换成同一种格式。
二进制模式就简单多了,直接读内存块,用 memcmp 或者自己逐字节比。但如果你要输出所有不同的位置,那就得自己写循环了。
核心设计图
文本模式的核心实现
文本模式说白了就是逐行扫描。我习惯用 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 配合大块读取,可能只需要几百次比较。
注意: 二进制模式下,不要用 fgets 或 fscanf,它们遇到 \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