文章目录
- 第一步代码
- 第一步代码分析(一)
- 第一步代码分析(二)
- 第二步代码
- 第二步代码分析
- 第三步代码
- 第三步代码分析
-
- [第三步代码分析:ASCII 区域的输出](#第三步代码分析:ASCII 区域的输出)
- [📊 完整的 hexdump 格式](#📊 完整的 hexdump 格式)
- [🔍 第三步代码详解](#🔍 第三步代码详解)
-
- [1️⃣ 为什么是 `" |%s|\n"` 格式?](#1️⃣ 为什么是
" |%s|\n"格式?) - [2️⃣ ascii 缓冲区的构建过程](#2️⃣ ascii 缓冲区的构建过程)
- [3️⃣ 为什么 ascii 需要 `+1` 长度?](#3️⃣ 为什么 ascii 需要
+1长度?) - [4️⃣ 输出效果示例](#4️⃣ 输出效果示例)
- [1️⃣ 为什么是 `" |%s|\n"` 格式?](#1️⃣ 为什么是
- [🎯 完整三步骤总结](#🎯 完整三步骤总结)
- [📝 完整输出示例(实际运行效果)](#📝 完整输出示例(实际运行效果))
- [💡 关键知识点](#💡 关键知识点)
- [🔧 可能的改进建议](#🔧 可能的改进建议)
- 使用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- 动态数组)
- [**`std::span` - 视图而非容器**](#
- [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'
核心原因
-
缓冲区大小与ASCII字符数不同:
- 要显示的ASCII字符正好是
buffer_size个(16个字节→16个ASCII字符) - 但C字符串需要一个额外的
'\0'终止符 来标记字符串结束
- 要显示的ASCII字符正好是
-
字符串操作需要终止符:
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个整数
关键要点总结
size=1, count=N:适合不确定长度的文件读取- 返回值 :实际读取的元素个数
- 循环条件 :
> 0表示还有数据可读 - 缓冲区安全 :确保
buffer至少有size * count字节空间 - 二进制安全 :不会对
\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..... |
💡 关键知识点
%s的安全性 :ascii 缓冲区有'\0'终止符,不会越界- 竖线包围:视觉上突出显示 ASCII 区域,便于与其他区域区分
- 固定宽度:即使最后一行数据不完整,ASCII 区域也固定显示16个字符(数据部分 + 空格填充)
.替换:不可打印字符显示为点,既美观又安全
🔧 可能的改进建议
如果想更专业,可以这样做:
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++ 版本!🎯