C++零基础到工程实战(4.3.7):Base16编码转换与解码实战——位运算、映射表与循环数组

张开发
2026/4/21 17:25:36 15 分钟阅读

分享文章

C++零基础到工程实战(4.3.7):Base16编码转换与解码实战——位运算、映射表与循环数组
目录一、本节学习内容概要图二、前言三、什么是 Base16 编码3.1 Base16 是什么意思3.2 为什么 16 个字符就能表示所有二进制数据3.3 一个字节为什么会变成两个字符四、编码部分原理详解4.1 编码表的作用4.2 为什么这里要用 unsigned char1char 本身通常也是 1 个字节2处理二进制字节时最稳妥的是 unsigned char4.3 如何取出高四位4.4 如何取出低四位4.5 为什么低四位前面要写 0b4.6 编码过程完整逻辑五、解码部分原理详解5.1 解码为什么要用映射表5.2 为什么 0~9 对应 48~57A~F 对应 65~705.3 解码时为什么每次循环加 25.4 左移四位再做或运算是什么意思1第一步高四位左移 4 位2第二步和低四位做按位或六、完整代码示例七、这段代码里几个特别容易混淆的知识点7.1 Base16 处理的是“字节”不是“字符个数”7.2 为什么这里既用了 range-for又用了普通 for7.3 为什么解码表里有很多 -17.4 提前预留字符串空间八、小结8.1 你学到的核心知识8.2 这一节真正练到的能力一、本节学习内容概要图二、前言在前面的数组与vector学习中我们已经接触过连续内存与下标访问循环遍历数组和字符串位运算的基本写法string与字符的关系这一节我们用一个非常实用的小案例把这些知识串起来Base16十六进制编码与解码。很多同学第一次看到 Base16会觉得它只是“把数字换个写法”。其实不是。它本质上是在做一件很重要的事把原始二进制字节数据转换成只包含可打印字符的字符串。比如网络传输、日志打印、文件内容展示、调试二进制数据时Base16 都非常常见。需要完整实现原始字符串 → Base16 编码字符串Base16 编码字符串 → 原始字符串恢复编码表定义和循环拆分字节的核心逻辑。解码时通过查表恢复数值再通过左移和或运算还原字节。三、什么是 Base16 编码3.1 Base16 是什么意思Base16就是16 进制表示法。它只使用 16 个字符0123456789ABCDEF这 16 个字符分别表示数值0~9表示 0 ~ 9A~F表示 10 ~ 15代码中编码表就是这样定义的0123456789ABCDEF3.2 为什么 16 个字符就能表示所有二进制数据因为二进制的4 位刚好可以表示0000 ~ 1111也就是十进制0 ~ 15总共正好 16 种情况。所以4 位二进制 → 1 个十六进制字符8 位二进制1 字节→ 2 个十六进制字符这就是Base16 的核心原理。3.3 一个字节为什么会变成两个字符一个字节是 8 位例如1000 0001可以拆成两部分高四位1000即十进制 8对应字符8低四位0001即十进制 1对应字符1所以这个字节编码后就是81也就是说1 个字节 → 2 个 Base16 字符四、编码部分原理详解4.1 编码表的作用编码表本质上就是一个映射关系static const string base16_enc_tab{0123456789ABCDEF};它表示下标 0 对应0下标 1 对应1...下标 10 对应A...下标 15 对应F也就是说base16_enc_tab[10] // 得到 A base16_enc_tab[15] // 得到 F这个设计非常巧妙因为只要我们先算出某个四位二进制对应的数值就能直接通过下标找到对应字符。4.2 为什么这里要用 unsigned char代码中遍历字符串时写的是for (unsigned char c : teststr)这里不用普通char而用unsigned char原因非常重要。1char本身通常也是 1 个字节但它到底是有符号还是无符号这和编译器实现有关不同平台可能不一样。很多环境下普通char可能会被当成有符号数处理。2处理二进制字节时最稳妥的是unsigned char因为Base16 编码处理的是“字节内容”字节应该统一看成0 ~ 255而unsigned char正好就是无符号 8 位整数范围更适合表示原始二进制数据。所以你这里的做法是对的处理原始字节数据优先使用unsigned char4.3 如何取出高四位代码char h c 4;假设当前字节0100 0001右移 4 位后0000 0100也就是十进制 4。所以h 4然后base16_enc_tab[h]就能得到字符4。4.4 如何取出低四位代码char l c 0b00001111;这一步叫做按位与取低四位。还是用这个字节举例0100 0001与上掩码0000 1111做按位与0100 0001 0000 1111 --------- 0000 0001结果就是 1。因此l 1再通过编码表base16_enc_tab[l]就得到1。4.5 为什么低四位前面要写 0b很多同学会问0b00001111前面的0b是什么0b表示这个字面量是二进制写法。也就是说0b00001111就是二进制的00001111十进制等于 15十六进制等于0x0F。所以这三种写法本质上都可以。那为什么这里更推荐0b00001111因为它最直观能直接看出来我们就是要保留低四位清掉高四位。所以这里写成二进制更适合教学也更容易理解位运算。4.6 编码过程完整逻辑编码部分整体逻辑如下string base16str; for (unsigned char c : teststr) { char h c 4; char l c 0b00001111; base16str base16_enc_tab[h]; base16str base16_enc_tab[l]; }这个过程可以概括为1逐字节遍历原始字符串2每个字节拆成高四位和低四位3高四位映射成一个 Base16 字符4低四位映射成一个 Base16 字符5拼接到结果字符串中最终得到的base16str就是可打印的十六进制字符串。五、解码部分原理详解在 Base16 转换中编码是将 4 位数值映射为十六进制字符解码则是将十六进制字符还原为对应的 4 位数值。5.1 解码为什么要用映射表解码时输入的是字符比如A F 8 0我们要先把它们恢复成数值A→ 10F→ 158→ 80→ 0所以你的代码里定义了一个解码表const vectorchar base16_dec_tab{ ... 0,1,2,3,4,5,6,7,8,9, //48~57 ... 10,11,12,13,14,15 //65~70 };这个表本质上是通过 ASCII 码直接查数值。5.2 为什么0~9对应 48~57A~F对应 65~70因为字符在计算机里本来就是按整数保存的每个字符都有对应编码值。例如0 - 48 9 - 57 A - 65 F - 70所以当你拿到一个字符时比如char ch A;实际上它对应的整数值就是 65。于是可以直接这样查表base16_dec_tab[ch]也就是base16_dec_tab[65] // 得到 10这就是“通过 ASCII 码做映射”的含义。5.3 解码时为什么每次循环加 2代码for (int i 0; i base16str.size(); i 2)因为原来 1 个字节被编码成了 2 个十六进制字符所以解码时必须两个字符一组来处理例如41这两个字符组合起来才表示原来的一个字节。所以循环步长必须是 2。5.4 左移四位再做或运算是什么意思这是解码最关键的一步ostr (h 4 | l);假设h 4; l 1;它们二进制分别是h 0100 l 00011第一步高四位左移 4 位0100 4 0100 00002第二步和低四位做按位或0100 0000 0000 0001 --------- 0100 0001最终恢复成原来的 1 个字节。所以 4把高四位放回高四位位置| l把低四位拼回去这就是完整的字节还原过程。六、完整代码示例#include iostream #include string #include vector using namespace std; // 编码表下标 0~15 映射到 0~F static const string base16_enc_tab{0123456789ABCDEF}; int main() { string teststr 测试用于base16的字符串; cout ori:\t teststr endl; // // 1. 编码原始数据 - Base16字符串 // string base16str; for (unsigned char c : teststr) { // 取高四位 char h c 4; // 取低四位 char l c 0b00001111; // 映射成两个Base16字符 base16str base16_enc_tab[h]; base16str base16_enc_tab[l]; } cout Base16:\t base16str endl; // // 2. 解码Base16字符串 - 原始数据 // string ostr; // 解码表通过字符ASCII码快速查出对应数值 const vectorchar base16_dec_tab{ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1, // 0~9 -1,-1,-1,-1,-1,-1,-1,-1,-1,-1, // 10~19 -1,-1,-1,-1,-1,-1,-1,-1,-1,-1, // 20~29 -1,-1,-1,-1,-1,-1,-1,-1,-1,-1, // 30~39 -1,-1,-1,-1,-1,-1,-1,-1, // 40~47 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, // 48~57 - 0~9 -1,-1,-1,-1,-1,-1,-1, // 58~64 10,11,12,13,14,15 // 65~70 - A~F }; for (int i 0; i base16str.size(); i 2) { char ch base16str[i]; char cl base16str[i 1]; unsigned char h base16_dec_tab[ch]; unsigned char l base16_dec_tab[cl]; // 高四位左移后与低四位拼接 ostr (h 4 | l); } cout Base16Decode:\t ostr endl; return 0; }七、这段代码里几个特别容易混淆的知识点7.1 Base16处理的是“字节”不是“字符个数”这一点非常关键。string里存的是字节序列。Base16 编码时本质上是对每个字节进行处理而不是按“你眼里看到几个汉字”处理。所以英文、数字在常见 ASCII/UTF-8 下通常 1 个字符对应 1 个字节中文则和编码方式有关不一定是 2 个字节例如UTF-8 下中文通常是3 个字节GBK 下中文常见是2 个字节因此不能简单地说一个字符一定两个字节。这个说法并不严谨。更准确的说法应该是Base16 编码按字节处理不同字符在不同编码下占用的字节数可能不同。7.2 为什么这里既用了 range-for又用了普通 for编码时for (unsigned char c : teststr)适合逐个字节读取写法简洁。解码时for (int i 0; i base16str.size(); i 2)因为必须两个字符一组处理所以需要下标访问。这也是一个很好的“循环数组/顺序表实战”案例逐元素遍历适合 range-for按固定步长分组处理适合普通下标 for7.3 为什么解码表里有很多 -1因为并不是所有 ASCII 字符都合法。例如0~9合法A~F合法其他字符很多都不合法所以用-1表示这个字符不是合法的 Base16 编码字符虽然你这份代码没有额外做非法字符检测但这个表的设计已经为后续扩展留好了位置。7.4 提前预留字符串空间例如base16str.reserve(teststr.size() * 2);因为编码后长度一定翻倍提前分配空间可以减少扩容次数提升效率。八、小结这一节的 Base16 编码与解码看起来只是一个字符串转换小案例但其实把很多 C 基础知识都串起来了8.1 你学到的核心知识1Base16 只使用 16 个字符表示二进制数据21 个字节会拆成高四位和低四位3高四位用右移 4获取4低四位用掩码 0b00001111获取5编码时通过字符串映射表查字符6解码时通过数组下标查数值7恢复原字节时要先左移四位再做按位或运算8处理原始字节数据时用unsigned char更稳妥9string处理的是字节序列不同字符占几个字节和编码方式有关8.2 这一节真正练到的能力这不只是“十六进制转换”。它实际上同时练到了位运算字符串遍历vector查表下标访问分组循环编码与解码思维原始数据与可打印字符串之间的转换思想这类代码在工程里非常常见尤其是网络协议二进制日志数据包打印文件内容调试加密与编码前处理所以这一节非常值得反复吃透。

更多文章