C++实现一个16进制打印Hexdump

hexdump

本文内容参考博主双笙子佯谬

我们将会实现一个如下效果的16进制展示器。

❯ ./hexdump -f CMakeCache.txt
00000000 23 20 54 68 69 73 20 69 73 20 74 68 65 20 43 4d |# This is the CM|
00000010 61 6b 65 43 61 63 68 65 20 66 69 6c 65 2e 0a 23 |akeCache file..#|
00000020 20 46 6f 72 20 62 75 69 6c 64 20 69 6e 20 64 69 | For build in di|
00000030 72 65 63 74 6f 72 79 3a 20 2f 68 6f 6d 65 2f 76 |rectory: /home/v|
00000040 69 76 65 6b 2f 43 6f 64 65 73 2f 73 6f 6d 65 5f |ivek/Codes/some_|
00000050 69 6d 70 6c 65 6d 65 6e 74 61 74 69 6f 6e 73 2f |implementations/|
00000060 68 65 78 64 75 6d 70 2f 6f 75 74 2f 62 75 69 6c |hexdump/out/buil|
00000070 64 2f 47 43 43 20 31 35 2e 32 2e 31 20 78 38 36 |d/GCC 15.2.1 x86|
00000080 5f 36 34 2d 70 63 2d 6c 69 6e 75 78 2d 67 6e 75 |_64-pc-linux-gnu|
00000090 0a 23 20 49 74 20 77 61 73 20 67 65 6e 65 72 61 |.# It was genera|
000000a0 74 65 64 20 62 79 20 43 4d 61 6b 65 3a 20 2f 75 |ted by CMake: /u|
000000b0 73 72 2f 62 69 6e 2f 63 6d 61 6b 65 0a 23 20 59 |sr/bin/cmake.# Y|
000000c0 6f 75 20 63 61 6e 20 65 64 69 74 20 74 68 69 73 |ou can edit this|
000000d0 20 66 69 6c 65 20 74 6f 20 63 68 61 6e 67 65 20 | file to change |
000000e0 76 61 6c 75 65 73 20 66 6f 75 6e 64 20 61 6e 64 |values found and|
000000f0 20 75 73 65 64 20 62 79 20 63 6d 61 6b 65 2e 0a | used by cmake..|
00000100 23 20 49 66 20 79 6f 75 20 64 6f 20 6e 6f 74 20 |# If you do not |
00000110 77 61 6e 74 20 74 6f 20 63 68 61 6e 67 65 20 61 |want to change *a|

实现思路

根据上述的形式,不难看出,需要先输出一个地址,指定宽度,然后输出每个字节的16进制数值(一行可以制订不同的数目,比如8/16等),最后打印出每个字节对应的ASCII码(在导入库使用的时候,如果每个元素的不是标准的uint8_t而是uint16_t甚至更大,我们就不打印后面的ASCII码).

template <typename Range>
    requires std::ranges::input_range<Range>
inline void hexdump(Range const &s, std::size_t one_line_num = 16) {
    using type = std::ranges::range_value_t<Range>;
    using UnsignedType = std::make_unsigned_t<type>;

    uint32_t addr = 0;
    std::vector<char> saved;
    for (auto chunk: s | std::views::chunk(one_line_num)) {
        std::cout << std::setw(8) << std::setfill('0') << std::hex << addr;
        for (auto const &c: chunk) {
            std::cout << " " << std::right << std::setw(2 * sizeof(type))
                      << std::hex << std::setfill('0')
                      << static_cast<unsigned long long>(
                             static_cast<UnsignedType>(c));
            ++addr;
            saved.push_back(c);
        }

        if constexpr (sizeof(type) == 1 && std::is_convertible_v<type, char>) {
            if (addr % one_line_num != 0) {
                for (std::size_t i = 0;
                     i < (one_line_num - addr % one_line_num) * 3; ++i) {
                    std::cout << " ";
                }
            }

            std::cout << " |";
            for (auto const &c0: saved) {
                char c = static_cast<unsigned char>(c0);
                if (std::isprint(c)) {
                    std::cout << c;
                } else {
                    std::cout << ".";
                }
            }
            std::cout << "|";
        }
        std::cout << "\n";
        saved.clear();
    }
}

函数前面和约束

template <typename Range>
    requires std::ranges::input_range<Range>
inline void hexdump(Range const &s, std::size_t one_line_num = 16)

我们函数接受两个参数,一个是满足ranges输入输出的可遍历的范围,one_line_num参数指定每行的个数。

类型推导

using type = std::ranges::range_value_t<Range>;        // 获取元素类型
using UnsignedType = std::make_unsigned_t<type>;       // 获取无符号版本
  • range_value_t:提取范围的元素类型(如 charint 等)
  • make_unsigned_t:转为无符号类型(如 charunsigned char

核心处理逻辑

uint32_t addr = 0;
std::vector<char> saved;  // 保存当前行的字符用于ASCII显示

// 将数据按 one_line_num 分块
for (auto chunk: s | std::views::chunk(one_line_num)) {
    // 打印地址
    std::cout << std::setw(8) << std::setfill('0') << std::hex << addr;
    
    // 处理每个块中的元素
    for (auto const &c: chunk) {
        // 打印十六进制值
        std::cout << " " << std::right << std::setw(2 * sizeof(type))
                  << std::hex << std::setfill('0')
                  << static_cast<unsigned long long>(
                         static_cast<UnsignedType>(c));
        ++addr;              // 地址递增
        saved.push_back(c);  // 保存字符用于ASCII显示
    }
  • setw(8):地址占8位宽度
  • setfill('0'):用0填充
  • hex:十六进制格式
  • setw(2 * sizeof(type)):每个值占的宽度(如 char 占2位,int 占8位)

ASCII显示

if constexpr (sizeof(type) == 1 && std::is_convertible_v<type, char>) {
    // 如果元素是1字节且可转换为char,显示ASCII
    if (addr % one_line_num != 0) {
        // 填充空格对齐
        for (std::size_t i = 0;
             i < (one_line_num - addr % one_line_num) * 3; ++i) {
            std::cout << " ";
        }
    }

    std::cout << " |";
    for (auto const &c0: saved) {
        char c = static_cast<unsigned char>(c0);
        std::cout << (std::isprint(c) ? c : '.');  // 可打印字符显示,否则显示.
    }
    std::cout << "|";
}

流程图解

数据: "Hello World!"

地址   十六进制值                    ASCII
0000: 48 65 6c 6c 6f 20 57 6f 72 6c 64 21    |Hello World!|
      ↑  ↑  ↑  ↑  ↑  ↑  ↑  ↑  ↑  ↑  ↑  ↑
      H  e  l  l  o     W  o  r  l  d  !

分块过程:
原始数据: [H][e][l][l][o][ ][W][o][r][l][d][!]
           chunk1(16) → 全部在一个块中

测试

#include "hexdump.hpp"
#include <cstring>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <iterator>
#include <string>

void print_usage() {
    std::cerr << "Options:\n"
              << "-f <file>   choose the file path\n"
              << "-x <size>   choose the one_line_size\n";
}

template <typename It>
struct Iter {
    It b, e;

    It begin() {
        return b;
    }

    It end() {
        return e;
    }

    It begin() const {
        return b;
    }

    It end() const {
        return e;
    }
};

int main(int argc, char **argv) {
    std::string context;
    std::ifstream file;
    std::size_t one_line_size = 16;

    for (std::size_t i = 1; i < argc; ++i) {
        std::string_view arg = argv[i];

        if (arg == "-h") {
            print_usage();
            std::exit(0);
        } else if (arg == "-x") {
            if (++i >= argc) {
                throw std::runtime_error("-x requires an argument");
            }
            int size = std::stoi(argv[i]);
            if (!size % 8) {
                throw std::runtime_error("wrong num of one_line_size");
            }
            one_line_size = size;
        } else if (arg == "-f") {
            if (++i >= argc) {
                throw std::runtime_error("-f requires an argument");
            }
            auto path = std::filesystem::path(std::string(argv[i]));
            file.open(path);
            if (!file.good()) {
                std::cerr << std::strerror(errno) << " (" << errno << ") "
                          << path << std::endl;
                exit(0);
            }
        } else {
            throw std::runtime_error(std::string("Unknown option: ") +
                                     std::string(arg));
        }
    }

    if (!file.is_open()) {
        std::istreambuf_iterator<char> begin{std::cin}, end{};
        hex::hexdump(Iter{begin, end}, one_line_size);
    } else {
        std::istreambuf_iterator<char> begin{file}, end{};
        hex::hexdump(Iter{begin, end}, one_line_size);
    }
}

参数部分

 for (std::size_t i = 1; i < argc; ++i) {
        std::string_view arg = argv[i];
     	......
 }

这部分是为了能够获取命令行参数读取文件或者指定,每行个数。

运行

if (!file.is_open()) {
    std::istreambuf_iterator<char> begin{std::cin}, end{};
    hex::hexdump(Iter{begin, end}, one_line_size);
} else {
    std::istreambuf_iterator<char> begin{file}, end{};
    hex::hexdump(Iter{begin, end}, one_line_size);
}

如果文件未打开,我们就使用文件的输入范围进行hex显示,否则从标准输入读取内容进行hex展示。

这里有一个结构体

template <typename It>
struct Iter {
    It b, e;

    It begin() {
        return b;
    }

    It end() {
        return e;
    }

    It begin() const {
        return b;
    }

    It end() const {
        return e;
    }
};

Iter 结构体的核心作用是把两个迭代器包装成一个合法的 range,这样就能用在需要 range 的地方(比如你的 hexdump 函数)。

// 迭代器对本身不是 range
std::istreambuf_iterator<char> begin{file}, end{};

// 不能直接传入 hexdump
hex::hexdump(begin, end);  // 错误!hexdump 需要 range,不是两个迭代器

// 用 Iter 包装后就可以了
hex::hexdump(Iter{begin, end});  // Iter 满足 range 概念!

之所以没有直接把文件的内容读取到std::string,一次性处理,是为了流式处理,防止大文件内存不足。

// 一次性读取
std::istringstream ss;
ss << file.rdbuf();
hex::hexdump(file,one_line_num);

// 流式处理
std::istreambuf_iterator<char> begin{file}, end{};

如此,既可以支持文件读取,也可以标准输入读取

❯ ./hexdump -f ../../../CMakeLists.txt
00000000 63 6d 61 6b 65 5f 6d 69 6e 69 6d 75 6d 5f 72 65 |cmake_minimum_re|
00000010 71 75 69 72 65 64 28 56 45 52 53 49 4f 4e 20 33 |quired(VERSION 3|
00000020 2e 31 30 2e 30 29 0a 70 72 6f 6a 65 63 74 28 68 |.10.0).project(h|
00000030 65 78 64 75 6d 70 20 56 45 52 53 49 4f 4e 20 30 |exdump VERSION 0|
00000040 2e 31 2e 30 20 4c 41 4e 47 55 41 47 45 53 20 43 |.1.0 LANGUAGES C|
00000050 20 43 58 58 29 0a 0a 73 65 74 28 43 4d 41 4b 45 | CXX)..set(CMAKE|
00000060 5f 43 58 58 5f 53 54 41 4e 44 41 52 44 20 32 33 |_CXX_STANDARD 23|
00000070 29 0a 0a 61 64 64 5f 65 78 65 63 75 74 61 62 6c |)..add_executabl|
00000080 65 28 68 65 78 64 75 6d 70 20 6d 61 69 6e 2e 63 |e(hexdump main.c|
00000090 70 70 29 0a 0a                                  |pp)..|
❯ ./hexdump
asdasdasdasdasdsa
00000000 61 73 64 61 73 64 61 73 64 61 73 64 61 73 64 73 |asdasdasdasdasds|
00000010 61 0a12349813267948612379461328467134
 31 32 33 34 39 38 31 33 32 36 37 39 34 38 |a.12349813267948|
00000020 36 31 32 33 37 39 34 36 31 33 32 38 34 36 37 31 |6123794613284671|
00000030 33 34 0ajsahdfgajhsdasjigdasjid
 6a 73 61 68 64 66 67 61 6a 68 73 64 61 |34.jsahdfgajhsda|
00000040 73 6a 69 67 64 61 73 6a 69 64 0a12345678
 31 32 33 34 35 |sjigdasjid.12345|
00000050 36 37 38 0a123456789123456
 31 32 33 34 35 36 37 38 39 31 32 33 |678.123456789123|
00000060 34 35 36 0a123432546546
 31 32 33 34 33 32 35 34 36 35 34 36 |456.123432546546|
00000070 0a2345325235235235
 32 33 34 35 33 32 35 32 33 35 32 33 35 32 33 |.234532523523523|
00000080 35 0a
posted @ 2026-03-18 19:58  大胖熊哈  阅读(3)  评论(0)    收藏  举报