三种 HexDump 实现对比:从 C 经典到现代 C++
在系统编程和底层调试中,以十六进制形式查看文件内容(Hex Dump)是一项基础而实用的需求。本文将展示三种不同风格的实现:Windows API 版 C 、标准 C 库版 C 以及 现代 C++ 版。通过对比,你不仅能学会写一个 Hex Dump 工具,还能体会到 C/C++ 语言数十年的演进。
一、什么是 Hex Dump?
Hex Dump 将文件的每个字节以十六进制形式输出,同时显示对应的可打印字符(不可打印字符用 . 代替)。典型的输出格式如下:
00000000 | 89 50 4E 47 0D 0A 1A 0A 00 00 00 0D 49 48 44 52 | .PNG........IHDR |
每行 16 个字节,依次显示偏移地址、十六进制数据、ASCII 表示。
二、版本一:标准 C 库(fopen / fread)
c
#include <stdio.h>
#include <ctype.h>
#include <string.h>
typedef unsigned char BYTE;
void HexAscii(const BYTE* data, size_t offset, size_t length) {
char ascii[17] = { 0 };
printf("%08X | ", (unsigned int)offset);
for (size_t i = 0; i < 16; i++) {
if (i < length) {
printf("%02X ", data[i]);
ascii[i] = isprint(data[i]) ? data[i] : '.';
} else {
printf(" ");
ascii[i] = ' ';
}
}
printf(" |%s|\n", ascii);
}
void HexDump(const char* fileName) {
FILE* pFile;
fopen_s(&pFile, fileName, "rb");
if (!pFile) return;
BYTE buffer[16] = { 0 };
size_t byteRead = 0, offset = 0;
while ((byteRead = fread(buffer, 1, sizeof(buffer), pFile)) > 0) {
HexAscii(buffer, offset, byteRead);
offset += byteRead;
}
fclose(pFile);
}
特点分析
- 可移植性:使用标准 C 库,可在任何支持 C 的平台上编译。
- 简单直观 :
fopen/fread是经典的文件读取方式。 - 不足 :
fopen_s是 C11 安全函数,Windows 上需要;Linux 可使用fopen。另外printf类型不匹配时容易产生警告。
三、版本二:Windows API(CreateFile / ReadFile)
c
#include <windows.h>
void HexDump(const char* fileName) {
HANDLE hFile = CreateFileA(fileName, GENERIC_READ, FILE_SHARE_READ,
NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == INVALID_HANDLE_VALUE) return;
BYTE buffer[16];
DWORD bytesRead = 0, offset = 0;
while (ReadFile(hFile, buffer, sizeof(buffer), &bytesRead, NULL) && bytesRead > 0) {
HexAscii(buffer, offset, bytesRead);
offset += bytesRead;
}
CloseHandle(hFile);
}
特点分析
- Windows 原生:直接调用操作系统 API,理论上可获得更好的底层控制(如异步 I/O、文件映射等)。
- 类型安全 :使用
DWORD等明确长度的类型,避免可移植性问题(但仅限于 Windows)。 - 不足 :不可移植,代码中混入
windows.h会增加编译依赖。
四、版本三:现代 C++(std::span / std::format)
cpp
#include <iostream>
#include <iomanip>
#include <fstream>
#include <format>
#include <array>
#include <span>
#include <cctype>
void HexAscii(std::span<const std::byte> data, std::uint32_t offset) {
std::array<char, 17> ascii{};
std::cout << std::format("{:08X} | ", offset);
for (std::size_t i = 0; i < 16; ++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] = ' ';
}
}
std::cout << std::format(" |{}|\n", ascii.data());
}
void HexDump(std::string_view filename) {
std::ifstream file(filename.data(), std::ios::binary);
if (!file) return;
std::array<std::byte, 16> 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);
}
}
现代特性解析
std::span:作为非拥有视图传递连续数据,避免了指针+长度的分离,类型安全且语义清晰。std::byte:C++17 引入的字节类型,明确表达"原始内存",避免char的歧义。std::format:C++20 的格式化库,类型安全、性能接近printf,不再需要繁琐的iomanip。std::array:替代 C 风格数组,提供 STL 接口(如.data()、.size())。std::ifstream:RAII 管理文件句柄,无需手动close。std::string_view:高效传递文件名,避免拷贝。
五、三种方案对比
| 特性 | 标准 C 库 | Windows API | 现代 C++ |
|---|---|---|---|
| 跨平台 | ✅ 优秀 | ❌ 仅 Windows | ✅ 优秀 |
| 类型安全 | ⚠️ 一般 | ⚠️ 一般 | ✅ 优秀 |
| 错误处理 | 需手动检查 | 需手动检查 | RAII + 异常 |
| 代码可读性 | ⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐⭐ |
| 格式化灵活性 | printf |
printf |
std::format |
| 依赖 C++ 标准 | C89 | - | C++20 |
| 二进制数据表示 | unsigned char* |
BYTE |
std::span<std::byte> |
六、如何选择?
- 维护老项目:标准 C 库版最稳妥,无需升级工具链。
- Windows 专用工具:可用 API 版,但现代 C++ 在 Windows 上同样高效且更安全。
- 新项目(C++17/20 可用) :毫不犹豫选择现代 C++ 版。
std::format和std::span显著提升代码质量和维护性。
七、扩展思考
- 性能优化 :现代 C++ 版本可以轻松改为一次读取更大块(如 4KB),然后循环调用
HexAscii处理每 16 字节,减少系统调用次数。 - 大文件支持 :当前偏移量使用
uint32_t,最大 4GB。实际生产代码应使用uint64_t或std::streamsize。 - 彩色输出:结合 ANSI 转义序列,可以让 ASCII 区域不同类别字符显示不同颜色。
- 命令行参数 :现代 C++ 版可轻松扩展
main(int argc, char** argv)接受用户输入的文件名。
结语
从 C 风格的指针运算到现代 C++ 的类型安全视图,Hex Dump 这个小工具折射出语言进化的光辉。std::span 和 std::format 让我们写出的代码既接近底层,又不失高级语言的表达力。希望本文能帮助你理解三种实现的设计思想,并在实际项目中做出合适的选择。
(完整代码已附于文中,复制即可编译运行。现代 C++ 版需要支持 C++20 的编译器,如 GCC 11+ / Clang 14+ / MSVC 2022。)