用 C++20 打造一个实用的十六进制对比工具

文章目录

用 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;
}

在日常开发中,我们经常需要查看文件的原始二进制内容,或者对比两个文件是否完全相同------甚至精确到每个字节的差异。虽然市面上有 hexdumpxxdBeyond 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);
}

三、双文件对比的实现

对比模式的核心挑战是:

  1. 两个文件可能长度不同。
  2. 需要并排显示,且差异字节要红色高亮。
  3. 同一行内既要显示文件 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 潜在改进

  1. 支持大文件:当前逐行读取,内存占用极小,已适合大文件。
  2. 跳过相同区域 :对比时可以检测连续相同的块,只输出差异部分(类似 diff 的压缩模式)。
  3. Windows 兼容性:ANSI 颜色码在 Windows 10+ 的终端中默认支持,旧系统可能需要启用 VT 模式。
  4. 宽字符处理:目前 ASCII 显示仅支持单字节字符,对于 UTF-8 文本可进一步优化。

六、总结

不到 150 行代码,我们就实现了一个功能完整的十六进制查看与对比工具。它展示了如何利用 C++20 的标准库组件高效地处理二进制文件,同时提供了友好的视觉反馈。无论你是需要快速检查文件内容,还是想学习现代 C++ 的 IO、格式化、内存视图等知识,这个例子都是一个不错的起点。

完整的代码已经贴在上面,你可以直接复制保存为 .cpp 文件编译运行。动手试试,用红色高亮抓住那些"狡猾"的字节差异吧!


相关推荐
代码中介商1 天前
C语言函数完全指南:从基础到实践
c语言·开发语言
hssfscv1 天前
软件设计师下午试题四——C语言(N皇后问题、分治、动态规划)
c语言·算法·动态规划
爱编码的小八嘎1 天前
C语言完美演绎8-7
c语言
幽灵诶1 天前
理解指针2
c语言
boonya1 天前
一文读懂MCP:AI连接万物的“USB-C接口”
c语言·开发语言·人工智能
yashuk1 天前
C语言条件编译三种方式及第一种方式的格式、作用与示例
c语言·程序设计·条件编译·代码示例·预处理程序
qeen871 天前
【数据结构】栈及其C语言模拟实现
c语言·数据结构·学习·
我不是懒洋洋1 天前
深入理解C语言指针:从一级指针到函数指针
c语言
熬夜敲代码的猫1 天前
C/C++:内存管理
c语言·c++·动态内存管理
云泽8081 天前
第十五届蓝桥杯大赛软件赛省赛C/C++大学B组
c语言·c++·算法·蓝桥杯