从0到1 编写HexDump工具

文章目录

  • 第一步代码
  • 第一步代码分析(一)
  • 第一步代码分析(二)
  • 第二步代码
  • 第二步代码分析
  • 第三步代码
  • 第三步代码分析
    • [第三步代码分析:ASCII 区域的输出](#第三步代码分析:ASCII 区域的输出)
    • [📊 完整的 hexdump 格式](#📊 完整的 hexdump 格式)
    • [🔍 第三步代码详解](#🔍 第三步代码详解)
      • [1️⃣ 为什么是 `" |%s|\n"` 格式?](#1️⃣ 为什么是 " |%s|\n" 格式?)
      • [2️⃣ ascii 缓冲区的构建过程](#2️⃣ ascii 缓冲区的构建过程)
      • [3️⃣ 为什么 ascii 需要 `+1` 长度?](#3️⃣ 为什么 ascii 需要 +1 长度?)
      • [4️⃣ 输出效果示例](#4️⃣ 输出效果示例)
    • [🎯 完整三步骤总结](#🎯 完整三步骤总结)
    • [📝 完整输出示例(实际运行效果)](#📝 完整输出示例(实际运行效果))
    • [💡 关键知识点](#💡 关键知识点)
    • [🔧 可能的改进建议](#🔧 可能的改进建议)
  • 使用C++改写代码
    • [📊 C++20 版本相比 C 版本的核心修改分析](#📊 C++20 版本相比 C 版本的核心修改分析)
      • [1️⃣ **类型安全性提升**](#1️⃣ 类型安全性提升)
      • [2️⃣ **资源管理(RAII vs 手动)**](#2️⃣ 资源管理(RAII vs 手动))
      • [3️⃣ **字符串处理**](#3️⃣ 字符串处理)
      • [4️⃣ **跨平台性**](#4️⃣ 跨平台性)
      • [5️⃣ **现代 C++ 特性的具体应用**](#5️⃣ 现代 C++ 特性的具体应用)
        • [**`std::span` - 视图而非容器**](#std::span - 视图而非容器)
        • [**`std::string_view` - 字符串视图**](#std::string_view - 字符串视图)
        • [**`std::vector` - 动态数组**](#std::vector - 动态数组)
      • [6️⃣ **错误处理对比**](#6️⃣ 错误处理对比)
      • [7️⃣ **代码简洁性对比**](#7️⃣ 代码简洁性对比)
      • [8️⃣ **性能对比**](#8️⃣ 性能对比)
      • [9️⃣ **具体修复的问题**](#9️⃣ 具体修复的问题)
      • [🔟 **总结:主要改进点**](#🔟 总结:主要改进点)
      • [✅ 何时使用 C++ 版本?](#✅ 何时使用 C++ 版本?)
      • [❌ 何时保留 C 版本?](#❌ 何时保留 C 版本?)

第一步代码

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS  // 禁用微软C运行时库中不安全函数(如fopen)的编译警告

#include <windows.h>      // 引入Windows API核心头文件,包含基本数据类型定义
#include <imagehlp.h>     // 引入ImageHlp库头文件,提供可执行文件映像加载/卸载函数(如MapAndLoad)
//#include <dbghelp.h>      // DbgHelp API(可选,与 ImageHlp 功能重叠)--- 符号处理与调试辅助库

#include <sstream>        // 引入字符串流库(本例未使用,可能预留给格式化输出)

constexpr size_t buffer_size = 16;   // 定义十六进制转储的每行字节数(16字节),编译期常量

// 函数:HexAscii - 以十六进制+ASCII格式打印单行内存数据
void HexAscii(CONST BYTE* data, SIZE_T offset, SIZE_T read_data_length)
{
	char ascii[buffer_size + 1] = { 0 };   // 存放ASCII表示(+1给结尾'\0'),并初始化为0
	ascii[buffer_size] = '\0';             // 手动将字符串最后一个字符设为'\0',确保安全

	printf("%08x | ", offset);             // 打印偏移量(8位十六进制,左补零),后跟竖线分隔符

	// 此处原代码省略了十六进制字节输出和ASCII拼接逻辑
	
	printf("\n");                          // 打印换行,结束当前行
}

// 函数:HexDump - 对指定二进制文件进行十六进制转储
VOID HexDump(CONST CHAR* file_name)
{
	FILE* file_p = fopen(file_name, "rb");   // 以二进制只读模式打开文件,返回文件指针
	if (!file_p)                             // 如果文件打开失败(指针为NULL)
	{
		printf("error");                     // 输出错误提示
		return;                              // 提前退出函数
	}

	BYTE buffer[buffer_size] = { 0 };        // 数据缓冲区,大小16字节,全部初始化为0
	SIZE_T byte_read = 0;                    // 记录实际读取到的字节数(每次循环)
	SIZE_T offset = 0;                       // 当前读取位置的文件偏移量(累计)

	// 循环读取文件:每次最多buffer_size字节,存入buffer,实际读取字节数存入byte_read
	while ((byte_read = fread(buffer, 1, buffer_size, file_p)) > 0)
	{
		HexAscii(buffer, offset, byte_read); // 调用HexAscii输出当前内存块的一行转储
		offset += byte_read;                 // 累计偏移量增加已读字节数
	}

	fclose(file_p);   // 关闭文件指针,释放资源
}

int main()   // 程序入口函数
{
	HexDump("Z:\\jpeg.jpg");   // 对Z盘根目录下的jpeg.jpg文件执行十六进制转储
	return 0;   // 返回0表示程序正常结束
}

第一步代码分析(一)

c 复制代码
char ascii[buffer_size + 1] = { 0 }; 为什么是+1而不是直接就是 char ascii[buffer_size ] ={0}

很好的问题!这涉及到C/C++字符串处理中的一个重要细节。

原因分析

cpp 复制代码
constexpr size_t buffer_size = 16;  // 每行显示16个字节

// 错误做法 ❌
char ascii[buffer_size] = {0};      // 只能存16个字符,没有终止符位置

// 正确做法 ✅  
char ascii[buffer_size + 1] = {0};  // 可存16个字符 + 1个终止符'\0'

核心原因

  1. 缓冲区大小与ASCII字符数不同

    • 要显示的ASCII字符正好是 buffer_size 个(16个字节→16个ASCII字符)
    • 但C字符串需要一个额外的 '\0' 终止符 来标记字符串结束
  2. 字符串操作需要终止符

    cpp 复制代码
    // 如果要使用这些函数,必须要有'\0'结尾
    printf("%s", ascii);     // 需要'\0'来知道在哪里停止
    strcpy(dest, ascii);     // 需要'\0'来复制
    strlen(ascii);           // 需要'\0'来计算长度

具体例子

cpp 复制代码
// 假设 buffer_size = 16
char wrong[16] = {0};                    // 索引 0-15,共16个位置
// 填入16个ASCII字符:索引0-15全是字符
// 问题:没有空间放'\0'了!

char correct[17] = {0};                  // 索引 0-16,共17个位置  
// 索引0-15填入16个ASCII字符
// 索引16自动是'\0'(因为初始化为0)

代码中的体现

在原始代码中:

cpp 复制代码
char ascii[buffer_size + 1] = {0};  // 17个元素:索引[0]到[16]
ascii[buffer_size] = '\0';          // 将索引[16]设为'\0'(其实已经做了,这行是冗余的)

因为用 {0} 初始化时,所有元素 都会被设为0,所以最后一行 ascii[buffer_size] = '\0'; 实际上是多余的,但写出来可以让意图更明确。

类比理解

就像高速公路有16条车道(显示16个字符),但你需要在终点后多一个缓冲区域放置"道路终点"标志('\0'),否则后面的车(字符串处理函数)不知道在哪里应该停下来。

第一步代码分析(二)

fread(buffer, 1, buffer_size, file_p) 详细解析

这是C标准库中的二进制文件读取函数,用于从文件中读取数据到内存缓冲区。

函数原型

cpp 复制代码
size_t fread(void *ptr, size_t size, size_t count, FILE *stream);

参数详解

参数 本例中的值 含义
buffer buffer 目标缓冲区指针:读取的数据存放的位置
size 1 每个元素的大小(字节数)
count buffer_size (16) 要读取的元素个数
stream file_p 文件指针:要读取的文件

工作原理解析

cpp 复制代码
fread(buffer, 1, buffer_size, file_p);
//      ↑     ↑        ↑
//     地址   大小     数量

为什么 size=1, count=16

这表示要读取 16个元素,每个元素1字节 = 总共16字节

等价于:

cpp 复制代码
// 方式1:size=1, count=16
fread(buffer, 1, 16, file_p);  // 常见用法:逐字节读取

// 方式2:size=16, count=1  
fread(buffer, 16, 1, file_p);  // 读取1个元素,每个16字节

两种写法的区别

cpp 复制代码
// 方法1:每次读1字节,读16次
fread(buffer, 1, 16, file_p);
// 返回值:实际读取的字节数(0-16)

// 方法2:每次读16字节,读1次  
fread(buffer, 16, 1, file_p);
// 返回值:实际读取的元素个数(0或1)

使用 size=1, count=N 的好处

  • 返回值就是实际读取的字节数(更精确)
  • 适合读取不确定大小的文件尾部(可能不足N字节)
  • 循环读取时更容易控制

返回值详解

cpp 复制代码
SIZE_T byte_read = fread(buffer, 1, buffer_size, file_p);
返回值 含义 示例(文件最后剩余5字节)
= buffer_size 成功读取完整16字节 文件前10次循环
< buffer_size 读取到文件末尾,剩余不足16字节 最后一次循环返回5
= 0 文件结束或发生错误 读取完所有数据后

完整执行示例

cpp 复制代码
// 假设文件大小为 38 字节(实际就是之前的 JPEG 文件)

while ((byte_read = fread(buffer, 1, 16, file_p)) > 0)
{
    // 第1次循环:读取字节0-15,byte_read = 16
    // 第2次循环:读取字节16-31,byte_read = 16  
    // 第3次循环:读取字节32-37,byte_read = 6(最后6字节)
    HexAscii(buffer, offset, byte_read);
    offset += byte_read;  // offset从0→16→32→38
}
// 第4次循环:fread返回0,循环退出

与其他读取方式对比

cpp 复制代码
// 1. fgetc() - 读单个字符(慢,每次1字节)
int ch = fgetc(file_p);  // 循环38次

// 2. fgets() - 读文本行(不适合二进制文件)
char line[256];
fgets(line, 256, file_p);  // 遇到换行或EOF停止

// 3. fread() - 批量读取(快速,适合二进制)
fread(buffer, 1, 16, file_p);  // 批量读取16字节,速度快

实际应用场景

cpp 复制代码
// 场景1:复制文件
char buf[4096];
while (size_t n = fread(buf, 1, 4096, src)) {
    fwrite(buf, 1, n, dst);
}

// 场景2:读取结构体
struct Student s;
fread(&s, sizeof(s), 1, file);  // 一次读入整个结构体

// 场景3:读取数组
int numbers[100];
fread(numbers, sizeof(int), 100, file);  // 读取100个整数

关键要点总结

  1. size=1, count=N:适合不确定长度的文件读取
  2. 返回值 :实际读取的元素个数
  3. 循环条件> 0 表示还有数据可读
  4. 缓冲区安全 :确保 buffer 至少有 size * count 字节空间
  5. 二进制安全 :不会对 \0\n 做特殊处理

这就是为什么原始代码中 fread 能正确读取JPEG等二进制文件(包含各种字节值,包括0)的原因!

第二步代码

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS  // 禁用不安全函数警告
#include <windows.h>      // Windows 基础类型定义
#include <imagehlp.h>     // ImageHlp API 函数声明(MapAndLoad, UnmapAndLoad 等)
//#include <dbghelp.h>      // DbgHelp API(可选,与 ImageHlp 功能重叠)
#include <sstream>
constexpr size_t buffer_size = 16;
//constexpr size_t buffer_size = 16;
void HexAscii(CONST BYTE* data, SIZE_T offset, SIZE_T read_data_length)
{
	char ascii[buffer_size + 1] = { 0 };
	ascii[buffer_size] = '\0';

	printf("%08x | ", offset);     //打印左边的偏移

	// 打印十六进制字节
	for (SIZE_T i = 0; i < buffer_size; i++)
	{
		
		if (i < read_data_length)//read_data_length可能不满buffer_size
		{
			printf("%02x ", data[i]); //打印中间的十六进制
			// 填充 ASCII 显示缓冲区
			//ascii[i] = (data[i] >= 0x20 && data[i] <= 0x7E) ? data[i] : '.';
			ascii[i] = isprint(data[i]) ? data[i] : '.';
		}
		else
		{
			printf("   ");  // 补齐空格
			ascii[i] = ' ';
		}

		// 每 8 个字节添加一个额外空格,提高可读性
		if (i == 7) printf("   ");////打印中间的十六进制的空格



	}

	printf("\n");
}

VOID HexDump(CONST CHAR* file_name)
{
	FILE* file_p = fopen(file_name, "rb");
	if (!file_p)
	{
		printf("error");
		return;
	}
	//constexpr size_t buffer_size = 16;

	BYTE buffer[buffer_size] = { 0 };
	SIZE_T byte_read = 0;
	SIZE_T offset = 0;
	// 修复:添加括号确保正确的运算顺序
	while ((byte_read = fread(buffer, 1, buffer_size, file_p)) > 0)
	{
		HexAscii(buffer, offset, byte_read);
		offset += byte_read;
	}

	// 添加:关闭文件
	fclose(file_p);
}

int main()
{

	HexDump("Z:\\jpeg.jpg");

	return 0;
}

第二步代码分析

好问题!for 循环里的 if-else 是为了处理最后一行数据不完整的情况。

核心原因

文件大小不一定是 buffer_size (16) 的整数倍,最后一次读取可能不足16字节

具体分析

cpp 复制代码
for (SIZE_T i = 0; i < buffer_size; i++)  // 固定循环16次(打印16个位置)
{
    if (i < read_data_length)              // 位置i有实际数据吗?
    {
        // 有数据:打印真实的十六进制字节
        printf("%02x ", data[i]);
        ascii[i] = isprint(data[i]) ? data[i] : '.';
    }
    else
    {
        // 没有数据:打印空格占位
        printf("   ");  // 3个空格,对齐格式
        ascii[i] = ' '; // ASCII部分也放空格
    }
}

实际执行示例

假设文件最后只剩 5个字节read_data_length = 5):

复制代码
buffer_size = 16(固定打印16列)

循环变量 i 从 0 到 15:

i=0: ✅ i<5 → 打印 data[0] (第一个字节)
i=1: ✅ i<5 → 打印 data[1] (第二个字节)
i=2: ✅ i<5 → 打印 data[2] (第三个字节)
i=3: ✅ i<5 → 打印 data[3] (第四个字节)
i=4: ✅ i<5 → 打印 data[4] (第五个字节)
i=5: ❌ i<5? 否 → 打印 "   " (三个空格,占位)
i=6: ❌ i<5? 否 → 打印 "   "
...
i=15: ❌ i<5? 否 → 打印 "   "

可视化效果

复制代码
偏移量    | 十六进制字节(16列)                    | ASCII
00000000 | 68 65 6c 6c 6f 20 77 6f  72 6c 64 0a    | hello world.  <- 完整行
0000000c | 74 68 69 73 20 69 73 20  61 20 74 65    | this is a te <- 完整行
00000018 | 73 74 2e                               | st.          <- 最后一行只有3字节

如果没有 if-else,最后一行会打印:

复制代码
00000018 | 73 74 2e 随机值 随机值 随机值 ...  | st.乱码乱码

导致显示垃圾数据

两种情况的对比

i 范围 read_data_length=16 read_data_length=5
0-4 打印真实数据 打印真实数据
5-15 打印真实数据 打印" "占位符

为什么每8字节加空格?

cpp 复制代码
if (i == 7) printf(" ");  // 第8个字节后加空格,分组显示

输出效果:

cpp 复制代码
// 没有分组(难看)
ff d8 ff e0 00 10 4a 46 49 46 00 01 01 00 00 01

// 有分组(易读)8字节+8字节
ff d8 ff e0 00 10 4a 46  49 46 00 01 01 00 00 01
//                     ↑ 这里多一个空格

完整流程示例

cpp 复制代码
// 假设文件大小 = 38 字节

// 第1次循环:offset=0, read_data_length=16 (完整)
// 打印 i=0-15 全部真实数据

// 第2次循环:offset=16, read_data_length=16 (完整)
// 打印 i=0-15 全部真实数据

// 第3次循环:offset=32, read_data_length=6 (最后6字节)
// i=0-5: 打印真实数据
// i=6-15: 打印空格占位符  ← 这里需要 else 分支!

总结

if-else 的作用就是:

  • 有数据:打印真实字节值
  • 无数据:打印空格保持对齐(视觉美观 + 避免显示垃圾数据)

没有这个判断,最后一行的未初始化缓冲区数据会显示乱码,破坏十六进制转储的标准格式。

第三步代码

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS  // 禁用不安全函数警告
#include <windows.h>      // Windows 基础类型定义
#include <imagehlp.h>     // ImageHlp API 函数声明(MapAndLoad, UnmapAndLoad 等)
//#include <dbghelp.h>      // DbgHelp API(可选,与 ImageHlp 功能重叠)
#include <sstream>
constexpr size_t buffer_size = 16;
//constexpr size_t buffer_size = 16;
void HexAscii(CONST BYTE* data, SIZE_T offset, SIZE_T read_data_length)
{
	char ascii[buffer_size + 1] = { 0 };
	ascii[buffer_size] = '\0';

	printf("%08x | ", offset);     //打印左边的偏移

	// 打印十六进制字节
	for (SIZE_T i = 0; i < buffer_size; i++)
	{
		
		if (i < read_data_length)//read_data_length可能不满buffer_size
		{
			printf("%02x ", data[i]); //打印中间的十六进制
			// 填充 ASCII 显示缓冲区
			//ascii[i] = (data[i] >= 0x20 && data[i] <= 0x7E) ? data[i] : '.';
			ascii[i] = isprint(data[i]) ? data[i] : '.';
		}
		else
		{
			printf("   ");  // 补齐空格
			ascii[i] = ' ';
		}

		// 每 8 个字节添加一个额外空格,提高可读性
		if (i == 7) printf("   ");////打印中间的十六进制的空格
	}

	//第三步代码,ascii累计存储完16个字符的结果之后输出
	printf(" |%s|\n", ascii);

	printf("\n");
}

VOID HexDump(CONST CHAR* file_name)
{
	FILE* file_p = fopen(file_name, "rb");
	if (!file_p)
	{
		printf("error");
		return;
	}
	//constexpr size_t buffer_size = 16;

	BYTE buffer[buffer_size] = { 0 };
	SIZE_T byte_read = 0;
	SIZE_T offset = 0;
	// 修复:添加括号确保正确的运算顺序
	while ((byte_read = fread(buffer, 1, buffer_size, file_p)) > 0)
	{
		HexAscii(buffer, offset, byte_read);
		offset += byte_read;
	}

	// 添加:关闭文件
	fclose(file_p);
}

int main()
{

	HexDump("Z:\\jpeg.jpg");

	return 0;
}

第三步代码分析

第三步代码分析:ASCII 区域的输出

第三步代码非常简单,就这一行:

cpp 复制代码
printf(" |%s|\n", ascii);

它的作用是打印 ASCII 可显示字符区域,完成整个 hexdump 的最后一列。

📊 完整的 hexdump 格式

第三步完成后,整行输出格式如下:

复制代码
00000000 | ff d8 ff e0 00 10 4a 46   49 46 00 01 01 00 00 01 | ÿØÿà..JFIF...... |
         ↑                           ↑                      ↑
      偏移量(8位)                   十六进制(51字符)       ASCII区域

🔍 第三步代码详解

1️⃣ 为什么是 " |%s|\n" 格式?

cpp 复制代码
printf(" |%s|\n", ascii);
//      ↑  ↑   ↑
//      |  |   └─ 换行符
//      |  └───── 打印 ascii 字符串
//      └──────── 前导空格 + 竖线分隔符

各部分含义

  • " ":一个空格,与十六进制区域分隔
  • "|":竖线分隔符,视觉上框出 ASCII 区域
  • "%s":打印 ascii 缓冲区的内容
  • "|":结束竖线
  • "\n":换行,准备打印下一行

2️⃣ ascii 缓冲区的构建过程

在第二步的循环中,ascii 数组已被填充:

cpp 复制代码
for (SIZE_T i = 0; i < buffer_size; i++)
{
    if (i < read_data_length)
    {
        // 可打印字符 → 原字符,不可打印 → '.'
        ascii[i] = isprint(data[i]) ? data[i] : '.';
    }
    else
    {
        ascii[i] = ' ';  // 无数据时填充空格
    }
}

填充示例 (假设数据为 "Hello\x01World"):

cpp 复制代码
data[0]='H' → isprint('H')=真 → ascii[0]='H'
data[1]='e' → isprint('e')=真 → ascii[1]='e'
data[2]='l' → isprint('l')=真 → ascii[2]='l'
data[3]='l' → isprint('l')=真 → ascii[3]='l'
data[4]='o' → isprint('o')=真 → ascii[4]='o'
data[5]=0x01 → isprint(0x01)=假 → ascii[5]='.'
data[6]='W' → isprint('W')=真 → ascii[6]='W'
data[7]='o' → isprint('o')=真 → ascii[7]='o'
...
data[15]=? → 可能为随机值或空格

3️⃣ 为什么 ascii 需要 +1 长度?

cpp 复制代码
char ascii[buffer_size + 1] = {0};  // 17 个元素
//    buffer_size=16 个字符 + 1个终止符

因为 printf("%s", ascii) 需要 '\0' 来知道字符串结束:

cpp 复制代码
索引: 0   1   2   ... 14  15 | 16
字符: H   e   l   ...  d   y | \0
                            ↑
                    自动初始化为0

4️⃣ 输出效果示例

cpp 复制代码
// 完整16字节的情况
printf(" |%s|\n", ascii);
// 输出: |Hello.World......|
// 注意:每个不可打印字符显示为'.'

// 最后一行不足16字节的情况
// ascii = "Hello     " (5个字符 + 11个空格)
printf(" |%s|\n", ascii);
// 输出: |Hello          |  (ASCII区域保持16字符宽度)

🎯 完整三步骤总结

步骤 代码位置 作用 输出示例
第1步 `printf("%08x ", offset)` 打印文件偏移量
第2步 for 循环 + printf("%02x ", data[i]) 打印16字节十六进制 ff d8 ff e0 ...
第3步 `printf(" %s \n", ascii)`

📝 完整输出示例(实际运行效果)

复制代码
00000000 | ff d8 ff e0 00 10 4a 46   49 46 00 01 01 00 00 01 | ÿØÿà..JFIF...... |
00000010 | 00 00 00 00 00 00 00 00   00 00 00 00 00 00 00 00 | ................ |
00000020 | 48 65 6c 6c 6f 20 57 6f   72 6c 64 00 00 00 00 00 | Hello World..... |

💡 关键知识点

  1. %s 的安全性 :ascii 缓冲区有 '\0' 终止符,不会越界
  2. 竖线包围:视觉上突出显示 ASCII 区域,便于与其他区域区分
  3. 固定宽度:即使最后一行数据不完整,ASCII 区域也固定显示16个字符(数据部分 + 空格填充)
  4. . 替换:不可打印字符显示为点,既美观又安全

🔧 可能的改进建议

如果想更专业,可以这样做:

cpp 复制代码
// 原代码
printf(" |%s|\n", ascii);

// 改进版(带颜色,仅Windows控制台)
printf(" |\033[32m%s\033[0m|\n", ascii);  // ASCII区域显示为绿色

// 或加边框修饰
printf(" |[%s]|\n", ascii);  // 方括号代替竖线

至此,你的 hexdump 工具的三个部分(偏移量、十六进制、ASCII)已经完整实现了!🎉

使用C++改写代码

cpp 复制代码
#include <iostream>
#include <fstream>
#include <iomanip>
#include <cctype>
#include <span>      // C++20 核心:视图
#include <vector>    // 动态数组
#include <cstdint>  // uint8_t

constexpr size_t WIDTH = 16;

void hexdump(std::string_view path) {
    std::ifstream file(path.data(), std::ios::binary);
    if (!file) {
        std::cerr << "error: cannot open " << path << '\n';
        return;
    }

    std::vector<uint8_t> buf(WIDTH);
    size_t offset = 0;

    while (file.read(reinterpret_cast<char*>(buf.data()), WIDTH) || file.gcount()) {
        // 修复1: 显式转换 streamsize 到 size_t
        auto count_raw = file.gcount();
        if (count_raw <= 0) break;

        size_t count = static_cast<size_t>(count_raw);

        // 偏移量
        std::cout << std::hex << std::setw(8) << std::setfill('0') << offset << " | ";

        // 十六进制(使用span,修复2: 使用 count 而不是 WIDTH)
        auto data = std::span<const uint8_t>(buf.data(), count);

        // 打印所有十六进制字节(固定 WIDTH 列)
        for (size_t i = 0; i < WIDTH; ++i) {
            if (i < data.size()) {
                std::cout << std::setw(2) << std::setfill('0')
                    << static_cast<unsigned int>(data[i]) << ' ';
            }
            else {
                std::cout << "   ";
            }
            if (i == 7) std::cout << ' ';
        }

        // ASCII 区域(只打印实际读取的字节)
        std::cout << " |";
        for (auto c : data) {
            std::cout << (std::isprint(c) ? static_cast<char>(c) : '.');
        }
        std::cout << "|\n";

        // 修复3: 安全的 offset 累加
        offset += count;
        file.clear();
    }
}

int main(int argc, char** argv) {
    //if (argc != 2) {
    //    std::cerr << "用法: " << argv[0] << " <文件>\n";
    //    return 1;
    //}

    try {
        hexdump("Z:\\jpeg.jpg");
    }
    catch (const std::exception& e) {
        std::cerr << "错误: " << e.what() << '\n';
        return 1;
    }
}

📊 C++20 版本相比 C 版本的核心修改分析

1️⃣ 类型安全性提升

方面 C 版本 C++20 版本 改进
数组传递 CONST BYTE* data + 单独长度 std::span<const uint8_t> 数据与长度绑定,防越界
缓冲区 BYTE buffer[16] 裸数组 std::vector<uint8_t> 动态安全,自动管理
类型转换 隐式转换 static_cast 显式转换 消除警告,意图明确
cpp 复制代码
// C: 分离的数据和长度,容易出错
void HexAscii(CONST BYTE* data, SIZE_T offset, SIZE_T read_data_length)

// C++: 数据+长度一体化,自动边界检查
void hexdump_line(std::span<const uint8_t> data, size_t offset)

2️⃣ 资源管理(RAII vs 手动)

资源 C 版本 C++20 版本
文件 fopen/fclose 手动 std::ifstream 自动关闭
缓冲区 固定大小栈数组 std::vector 可扩展(虽然本例固定)
错误处理 返回值检查 异常 + RAII
cpp 复制代码
// C: 忘记 fclose 会泄漏
FILE* file_p = fopen(...);
if (!file_p) return;
// ... 多个返回路径,容易忘记 fclose
fclose(file_p);

// C++: 析构函数自动关闭,不可能泄漏
std::ifstream file(path, std::ios::binary);
if (!file) return;
// 离开作用域自动关闭

3️⃣ 字符串处理

操作 C 版本 C++20 版本
文件名参数 CONST CHAR* std::string_view
ASCII 缓冲区 固定数组 + 手动 \0 std::span 视图 + std::isprint
格式化输出 printf(类型不安全) std::cout + std::setw(类型安全)
cpp 复制代码
// C: printf 格式字符串与参数类型不匹配时未定义行为
printf("%08x | ", offset);     // offset 是 SIZE_T,但 %x 期望 unsigned int

// C++: 编译期类型检查
std::cout << std::hex << std::setw(8) << std::setfill('0') << offset << " | ";

4️⃣ 跨平台性

依赖 C 版本 C++20 版本
Windows SDK 需要 windows.h, imagehlp.h 不需要
类型定义 BYTE, SIZE_T (Windows 特有) uint8_t, size_t (标准 C++)
编译器限制 仅 Windows 任何支持 C++20 的编译器

5️⃣ 现代 C++ 特性的具体应用

std::span - 视图而非容器
cpp 复制代码
// 不拷贝数据,只创建视图
auto data = std::span<const uint8_t>(buf.data(), count);

// 自动知道大小,避免单独的 length 参数
for (auto c : data) { ... }  // 范围 for 循环
std::string_view - 字符串视图
cpp 复制代码
void hexdump(std::string_view path)  // 接受字符串字面量、std::string 等
std::vector - 动态数组
cpp 复制代码
std::vector<uint8_t> buf(WIDTH);  // 完全初始化
// 可轻松改为更大的缓冲区:buf.resize(1024);

6️⃣ 错误处理对比

场景 C 版本 C++20 版本
文件打开失败 printf("error") std::cerr + return
类型转换溢出 静默丢失 显式转换 + 可加检查
异常情况 不可用 try-catch

7️⃣ 代码简洁性对比

指标 C 版本 C++20 版本
行数 ~75 行 ~50 行
手动资源管理 2处 (fopen/fclose) 0处
长度参数传递 每个函数都要 0(用 span 包装)
类型转换警告 可能有 显式处理

8️⃣ 性能对比

方面 C 版本 C++20 版本 差异
内存分配 栈分配 vector 堆分配(一次性) 可忽略
格式化 printf(快) std::cout(稍慢) 可接受
视图创建 std::span(零开销) 相同
范围检查 手动 自动但可关闭 相同

9️⃣ 具体修复的问题

cpp 复制代码
// 问题1: 隐式转换警告(C 版本)
offset += byte_read;  // SIZE_T = unsigned, 可能溢出

// 修复: 显式转换 + 安全检查
size_t count = static_cast<size_t>(count_raw);
if (count > max_possible) { /* 处理 */ }
offset += count;
cpp 复制代码
// 问题2: 数组越界风险
char ascii[buffer_size + 1];
ascii[buffer_size] = '\0';  // 索引从0开始,buffer_size 是有效索引

// C++ 版本使用 span,自动边界
std::span<const uint8_t> data(buf.data(), count);
for (auto c : data) { ... }  // 不会越界

🔟 总结:主要改进点

# 改进项 价值
1 类型安全 编译期捕获更多错误
2 RAII 消除资源泄漏风险
3 跨平台 可在 Linux/macOS 编译运行
4 std::span 简化 API,减少参数传递
5 现代格式化 类型安全的输出
6 异常处理 统一的错误处理机制
7 可读性 意图更明确的代码
8 可维护性 更容易修改和扩展

✅ 何时使用 C++ 版本?

  • 需要跨平台支持(Linux/macOS/Windows)
  • 项目本身就是 C++ 项目
  • 需要更好的类型安全
  • 希望减少手动资源管理

❌ 何时保留 C 版本?

  • 嵌入式系统或特殊环境
  • 库需要 C ABI 接口
  • 极简主义代码(不依赖 STL)
  • 遗留系统维护

C++20 版本在不损失性能 的前提下,提供了更好的安全性、可读性和可维护性。对于新项目,推荐使用 C++ 版本!🎯

相关推荐
ghie90901 小时前
4轴运动控制源代码(STM32 + GRBL 1.1移植版)
stm32·单片机·嵌入式硬件
0南城逆流02 小时前
【STM32】RTT-Studio中HAL库开发教程十一:WS2812彩色RGB模块使用
stm32·单片机·嵌入式硬件
恶魔泡泡糖3 小时前
stm32F103C8T6标准库外部中断点灯
stm32·单片机·嵌入式硬件
The_superstar63 小时前
衡山派LVGL注意点
单片机·lvgl·衡山派·俊杰
fengfuyao9853 小时前
STM32 ADC音频采样与FFT频谱分析实现
stm32·嵌入式硬件·音视频
张海森-1688203 小时前
海思原生 isp调试工具使用方法
单片机
踏着七彩祥云的小丑3 小时前
嵌入式测试学习第 4 天:集成电路、芯片、FPGA
单片机·嵌入式硬件
项目題供诗3 小时前
STM32-对射式红外传感器计次&旋转编码器计次(九)
人工智能·stm32·嵌入式硬件
llilian_163 小时前
如何甄选专业级失真度测量仪校准装置
人工智能·功能测试·单片机·嵌入式硬件·测试工具·51单片机