文章目录
- [用 C++20 打造一个实用的十六进制对比工具](#用 C++20 打造一个实用的十六进制对比工具)
-
- 完整代码
- 一、整体设计
-
- [1.1 基本常量与数据结构](#1.1 基本常量与数据结构)
- [1.2 核心函数](#1.2 核心函数)
- 二、单文件转储的实现
- 三、双文件对比的实现
-
- [3.1 差异检测与着色](#3.1 差异检测与着色)
- [3.2 并排布局](#3.2 并排布局)
- [3.3 读取控制](#3.3 读取控制)
- 四、使用示例
-
- [4.1 编译](#4.1 编译)
- [4.2 命令行用法](#4.2 命令行用法)
- [4.3 输出效果](#4.3 输出效果)
- 五、代码亮点与改进方向
-
- [5.1 现代 C++ 特性运用](#5.1 现代 C++ 特性运用)
- [5.2 可扩展性](#5.2 可扩展性)
- [5.3 潜在改进](#5.3 潜在改进)
- 六、总结
用 C++20 打造一个实用的十六进制对比工具
完整代码
cpp
#include <iostream>
#include <iomanip>
#include <fstream>
#include <format>
#include <array>
#include <span>
#include <cctype>
#include <string_view>
#include <string>
// ANSI 颜色代码
constexpr const char* COL_RESET = "\033[0m";
constexpr const char* COL_RED = "\033[31m";
constexpr std::size_t BYTES_PER_LINE = 4; // 每行显示的字节数
// 单文件:打印一行(4字节十六进制 + ASCII)
void HexAscii(std::span<const std::byte> data, std::uint32_t offset) {
std::array<char, BYTES_PER_LINE + 1> ascii{}; // ASCII部分 + '\0'
std::cout << std::format("{:08X} | ", offset);
// 十六进制部分
for (std::size_t i = 0; i < BYTES_PER_LINE; ++i) {
if (i < data.size()) {
unsigned char c = static_cast<unsigned char>(data[i]);
std::cout << std::format("{:02X} ", c);
ascii[i] = std::isprint(c) ? static_cast<char>(c) : '.';
}
else {
std::cout << " "; // 对齐缺失字节
ascii[i] = ' ';
}
}
ascii[BYTES_PER_LINE] = '\0';
std::cout << std::format(" |{}|\n", ascii.data());
}
// 对比模式:打印一行(两个文件各4字节十六进制 + ASCII,并排)
void CompareHexLine(std::span<const std::byte> dataA,
std::span<const std::byte> dataB,
std::uint32_t offset) {
// 输出偏移量
std::cout << std::format("{:08X} | ", offset);
// ---- 文件 A 的十六进制部分 ----
for (std::size_t i = 0; i < BYTES_PER_LINE; ++i) {
bool diff = false;
if (i < dataA.size() && i < dataB.size()) {
diff = (dataA[i] != dataB[i]);
}
else if (i < dataA.size() || i < dataB.size()) {
diff = true; // 一方有数据,另一方无
}
// 双方都无数据时不标记差异
if (diff) std::cout << COL_RED;
if (i < dataA.size()) {
unsigned char c = static_cast<unsigned char>(dataA[i]);
std::cout << std::format("{:02X} ", c);
}
else {
std::cout << "-- ";
}
if (diff) std::cout << COL_RESET;
}
// 文件 A 的 ASCII 部分
std::cout << "| ";
for (std::size_t i = 0; i < BYTES_PER_LINE; ++i) {
bool diff = false;
if (i < dataA.size() && i < dataB.size()) {
diff = (dataA[i] != dataB[i]);
}
else if (i < dataA.size() || i < dataB.size()) {
diff = true;
}
if (diff) std::cout << COL_RED;
if (i < dataA.size()) {
unsigned char c = static_cast<unsigned char>(dataA[i]);
std::cout << (std::isprint(c) ? static_cast<char>(c) : '.');
}
else {
std::cout << ' ';
}
if (diff) std::cout << COL_RESET;
}
// 分隔符,准备文件 B
std::cout << " | ";
// ---- 文件 B 的十六进制部分 ----
for (std::size_t i = 0; i < BYTES_PER_LINE; ++i) {
bool diff = false;
if (i < dataA.size() && i < dataB.size()) {
diff = (dataA[i] != dataB[i]);
}
else if (i < dataA.size() || i < dataB.size()) {
diff = true;
}
if (diff) std::cout << COL_RED;
if (i < dataB.size()) {
unsigned char c = static_cast<unsigned char>(dataB[i]);
std::cout << std::format("{:02X} ", c);
}
else {
std::cout << "-- ";
}
if (diff) std::cout << COL_RESET;
}
// 文件 B 的 ASCII 部分
std::cout << "| ";
for (std::size_t i = 0; i < BYTES_PER_LINE; ++i) {
bool diff = false;
if (i < dataA.size() && i < dataB.size()) {
diff = (dataA[i] != dataB[i]);
}
else if (i < dataA.size() || i < dataB.size()) {
diff = true;
}
if (diff) std::cout << COL_RED;
if (i < dataB.size()) {
unsigned char c = static_cast<unsigned char>(dataB[i]);
std::cout << (std::isprint(c) ? static_cast<char>(c) : '.');
}
else {
std::cout << ' ';
}
if (diff) std::cout << COL_RESET;
}
std::cout << " |\n";
}
// 单文件十六进制转储
void HexDump(std::string_view filename) {
std::ifstream file(filename.data(), std::ios::binary);
if (!file) {
std::cerr << std::format("Failed to open file: {}\n", filename);
return;
}
std::array<std::byte, BYTES_PER_LINE> buffer{};
std::uint32_t offset = 0;
while (file.read(reinterpret_cast<char*>(buffer.data()), buffer.size())) {
HexAscii(std::span<const std::byte>(buffer), offset);
offset += static_cast<std::uint32_t>(buffer.size());
}
if (file.gcount() > 0) {
auto lastSpan = std::span<const std::byte>(buffer.data(), file.gcount());
HexAscii(lastSpan, offset);
}
}
// 两文件十六进制对比
void HexCompare(std::string_view file1, std::string_view file2) {
std::ifstream f1(file1.data(), std::ios::binary);
std::ifstream f2(file2.data(), std::ios::binary);
if (!f1) {
std::cerr << std::format("Failed to open file: {}\n", file1);
return;
}
if (!f2) {
std::cerr << std::format("Failed to open file: {}\n", file2);
return;
}
std::array<std::byte, BYTES_PER_LINE> buf1{}, buf2{};
std::uint32_t offset = 0;
while (true) {
std::streamsize count1 = 0, count2 = 0;
if (!f1.eof()) {
f1.read(reinterpret_cast<char*>(buf1.data()), buf1.size());
count1 = f1.gcount();
}
if (!f2.eof()) {
f2.read(reinterpret_cast<char*>(buf2.data()), buf2.size());
count2 = f2.gcount();
}
if (count1 == 0 && count2 == 0)
break;
CompareHexLine(std::span<const std::byte>(buf1.data(), count1),
std::span<const std::byte>(buf2.data(), count2),
offset);
offset += BYTES_PER_LINE;
}
}
// 帮助信息
void PrintUsage(const char* progName) {
std::cout << "Usage:\n"
<< " " << progName << " <file> # 十六进制转储(每行4字节+ASCII)\n"
<< " " << progName << " <file1> <file2> # 对比两个文件(每行4字节并排+ASCII)\n";
}
//
//int main(int argc, char* argv[]) {
// if (argc == 2) {
// HexDump(argv[1]);
// }
// else if (argc == 3) {
// HexCompare(argv[1], argv[2]);
// }
// else {
// PrintUsage(argv[0]);
// return 1;
// }
// return 0;
//}
int main(int argc, char* argv[]) {
HexCompare("C:\\Code\\test.png", "C:\\Code\\test2.png");
return 0;
}
在日常开发中,我们经常需要查看文件的原始二进制内容,或者对比两个文件是否完全相同------甚至精确到每个字节的差异。虽然市面上有 hexdump、xxd、Beyond Compare 等工具,但自己动手实现一个轻量级的十六进制查看器兼对比器,不仅有趣,还能加深对 C++ 文件流、std::span、格式化输出等特性的理解。
本文介绍的代码实现了一个命令行工具,支持两种模式:
- 单文件模式:以每行 4 字节的格式输出十六进制和对应的 ASCII 字符。
- 双文件对比模式:并排显示两个文件的十六进制和 ASCII,并用红色高亮标记差异字节。
完整代码基于 C++20 标准库编写(<format>、<span> 等),不依赖任何第三方库。下面我们来详细拆解它的设计思路和实现细节。
一、整体设计
1.1 基本常量与数据结构
cpp
constexpr const char* COL_RESET = "\033[0m";
constexpr const char* COL_RED = "\033[31m";
constexpr std::size_t BYTES_PER_LINE = 4;
- 使用 ANSI 转义序列控制终端颜色,差异部分用红色突出。
BYTES_PER_LINE控制每行显示的字节数(这里设为 4,方便排版,也可根据需要调整)。
1.2 核心函数
HexAscii():打印单行数据(4 字节十六进制 + ASCII)。CompareHexLine():并排打印两个文件的同一行,并标出不同之处。HexDump():单文件转储主逻辑。HexCompare():双文件对比主逻辑。PrintUsage():帮助信息。
所有函数均接受 std::span<const std::byte> 作为数据视图,避免了不必要的拷贝。
二、单文件转储的实现
cpp
void HexAscii(std::span<const std::byte> data, std::uint32_t offset) {
std::array<char, BYTES_PER_LINE + 1> ascii{};
std::cout << std::format("{:08X} | ", offset);
// 十六进制部分
for (std::size_t i = 0; i < BYTES_PER_LINE; ++i) {
if (i < data.size()) {
unsigned char c = static_cast<unsigned char>(data[i]);
std::cout << std::format("{:02X} ", c);
ascii[i] = std::isprint(c) ? static_cast<char>(c) : '.';
} else {
std::cout << " "; // 对齐缺失字节
ascii[i] = ' ';
}
}
ascii[BYTES_PER_LINE] = '\0';
std::cout << std::format(" |{}|\n", ascii.data());
}
- 偏移量 :使用
{:08X}输出 8 位十六进制地址。 - 十六进制列:每个字节两位十六进制数加一个空格,不足 4 字节时用三个空格补齐。
- ASCII 列 :可打印字符原样显示,不可打印字符显示为
.,最后用|包裹。 - 每行恰好处理
BYTES_PER_LINE个字节(最后一行可能不足)。
主循环利用 std::ifstream::read() 读取固定大小的缓冲区,最后用 gcount() 处理剩余字节:
cpp
std::array<std::byte, BYTES_PER_LINE> buffer{};
while (file.read(reinterpret_cast<char*>(buffer.data()), buffer.size())) {
HexAscii(std::span<const std::byte>(buffer), offset);
offset += BYTES_PER_LINE;
}
if (file.gcount() > 0) {
auto lastSpan = std::span<const std::byte>(buffer.data(), file.gcount());
HexAscii(lastSpan, offset);
}
三、双文件对比的实现
对比模式的核心挑战是:
- 两个文件可能长度不同。
- 需要并排显示,且差异字节要红色高亮。
- 同一行内既要显示文件 A 的十六进制 + ASCII,又要显示文件 B 的十六进制 + ASCII。
3.1 差异检测与着色
在 CompareHexLine() 中,我们为每个字节位置计算一个 diff 标志:
- 如果两个文件在该位置都有数据但值不同 →
diff = true - 如果一个文件有数据而另一个没有(即文件长度不一致)→
diff = true
然后根据 diff 决定是否包裹红色 ANSI 码:
cpp
if (diff) std::cout << COL_RED;
// 输出字节或占位符
if (diff) std::cout << COL_RESET;
3.2 并排布局
输出格式为:
偏移量 | 文件A十六进制 | 文件A的ASCII | 文件B十六进制 | 文件B的ASCII |
示例:
00000000 | 89 50 4E 47 | .PNG | 89 50 4E 47 | .PNG |
00000004 | 0D 0A 1A 0A | .... | 0D 0A 1A 0A | .... |
...
实际代码中通过多次循环分别输出文件 A 的 hex、A 的 ASCII、B 的 hex、B 的 ASCII,每个部分独立判断差异并着色。
3.3 读取控制
由于两个文件长度可能不同,我们分别读取各自的一块数据,然后取有效长度调用 CompareHexLine():
cpp
std::streamsize count1 = 0, count2 = 0;
if (!f1.eof()) { f1.read(...); count1 = f1.gcount(); }
if (!f2.eof()) { f2.read(...); count2 = f2.gcount(); }
if (count1 == 0 && count2 == 0) break;
CompareHexLine(std::span(buf1.data(), count1), std::span(buf2.data(), count2), offset);
offset += BYTES_PER_LINE;
这样即使一个文件先读完,另一文件剩余的部分也会被正常显示,缺失处用 -- 和空格表示。
四、使用示例
4.1 编译
使用支持 C++20 的编译器(如 GCC 11+、Clang 14+、MSVC 2022):
bash
g++ -std=c++20 -o hexdiff hexdiff.cpp
4.2 命令行用法
-
单文件转储 :
bash./hexdiff file.bin -
双文件对比 :
bash./hexdiff file1.bin file2.bin
如果你不想每次手动输入参数,也可以在 main() 中直接硬编码路径(如原代码最后的示例):
cpp
int main() {
HexCompare("C:\\Code\\test.png", "C:\\Code\\test2.png");
return 0;
}
4.3 输出效果
单文件输出:
00000000 | 89 50 4E 47 | .PNG |
00000004 | 0D 0A 1A 0A | .... |
对比模式(差异部分红色显示):
00000000 | 89 50 4E 47 | .PNG | 89 50 4F 47 | .POG |
^^^^ ^^^^
(注:实际红色由终端支持,这里用 ^^^ 示意)
五、代码亮点与改进方向
5.1 现代 C++ 特性运用
std::span:避免指针+长度传递,安全且语义清晰。std::format:类型安全的格式化输出,比printf更现代。std::byte:明确表示原始内存,避免字符类型的歧义。std::array:固定大小缓冲区,性能优于vector。
5.2 可扩展性
- 调整
BYTES_PER_LINE即可改变每行字节数,例如 8 或 16。 - 可以增加命令行参数解析(如
-c开启对比、-w设置列宽),目前简单但够用。
5.3 潜在改进
- 支持大文件:当前逐行读取,内存占用极小,已适合大文件。
- 跳过相同区域 :对比时可以检测连续相同的块,只输出差异部分(类似
diff的压缩模式)。 - Windows 兼容性:ANSI 颜色码在 Windows 10+ 的终端中默认支持,旧系统可能需要启用 VT 模式。
- 宽字符处理:目前 ASCII 显示仅支持单字节字符,对于 UTF-8 文本可进一步优化。
六、总结
不到 150 行代码,我们就实现了一个功能完整的十六进制查看与对比工具。它展示了如何利用 C++20 的标准库组件高效地处理二进制文件,同时提供了友好的视觉反馈。无论你是需要快速检查文件内容,还是想学习现代 C++ 的 IO、格式化、内存视图等知识,这个例子都是一个不错的起点。
完整的代码已经贴在上面,你可以直接复制保存为 .cpp 文件编译运行。动手试试,用红色高亮抓住那些"狡猾"的字节差异吧!