43、C++项目实战:Base64编解码工具
Base64 编码,说白了就是一种把二进制数据转成文本的“翻译规则”。
我最早接触它,是在做网络协议解析的时候。那时候要传一张图片,结果发现 HTTP 协议里有些字符会被截断或误解。嗯,Base64 就是来解决这个问题的——它只用了 64 个“安全字符”,就能表示任意二进制数据。
Base64 的原理,一句话讲清楚
Base64 的核心思想很简单:把 3 个字节(24 位)拆成 4 组,每组 6 位,然后映射到一张字符表上。
你想想看,6 位二进制能表示 0~63,正好对应 64 个字符。这就是“Base64”名字的由来。
核心公式:
3 字节 → 4 个 Base64 字符
如果字节数不是 3 的倍数,末尾补 0,并用 '=' 填充。
字符表长这样:
| 索引 | 字符 | 索引 | 字符 | 索引 | 字符 | 索引 | 字符 |
|---|---|---|---|---|---|---|---|
| 0 | A | 16 | Q | 32 | g | 48 | w |
| 1 | B | 17 | R | 33 | h | 49 | x |
| ... | ... | ... | ... | ... | ... | ... | ... |
| 62 | + | 63 | / | 填充 | = |
我个人习惯把这张表记成:A-Z(0-25)、a-z(26-51)、0-9(52-61)、+(62)、/(63)。
编码流程,我画了张图
下面这张 SVG 图,把编码过程拆成了 4 步。你看完就明白了。
代码实现,我踩过的坑
直接上代码。我写了一个完整的 Base64 编解码类,里面加了不少注释。
#include <iostream>
#include <string>
#include <vector>
#include <cstdint>
class Base64 {
private:
// 编码表
static const char kEncodeTable[65];
// 解码表,-1 表示非法字符
static const int kDecodeTable[256];
public:
// 编码:输入字节数组,输出 Base64 字符串
static std::string Encode(const std::vector<uint8_t>& data) {
std::string result;
size_t i = 0;
size_t len = data.size();
while (i + 3 <= len) {
// 取 3 个字节,拼成 24 位整数
uint32_t val = (data[i] << 16) | (data[i+1] << 8) | data[i+2];
// 拆成 4 个 6 位索引
result += kEncodeTable[(val >> 18) & 0x3F];
result += kEncodeTable[(val >> 12) & 0x3F];
result += kEncodeTable[(val >> 6) & 0x3F];
result += kEncodeTable[val & 0x3F];
i += 3;
}
// 处理剩余字节
size_t remain = len - i;
if (remain == 1) {
uint32_t val = data[i] << 16;
result += kEncodeTable[(val >> 18) & 0x3F];
result += kEncodeTable[(val >> 12) & 0x3F];
result += "==";
} else if (remain == 2) {
uint32_t val = (data[i] << 16) | (data[i+1] << 8);
result += kEncodeTable[(val >> 18) & 0x3F];
result += kEncodeTable[(val >> 12) & 0x3F];
result += kEncodeTable[(val >> 6) & 0x3F];
result += "=";
}
return result;
}
// 解码:输入 Base64 字符串,输出原始字节数组
static std::vector<uint8_t> Decode(const std::string& input) {
std::vector<uint8_t> result;
size_t len = input.size();
// 去掉末尾的 '='
size_t padding = 0;
while (len > 0 && input[len-1] == '=') {
padding++;
len--;
}
for (size_t i = 0; i + 4 <= len; i += 4) {
int a = kDecodeTable[(uint8_t)input[i]];
int b = kDecodeTable[(uint8_t)input[i+1]];
int c = kDecodeTable[(uint8_t)input[i+2]];
int d = kDecodeTable[(uint8_t)input[i+3]];
// 检查非法字符
if (a == -1 || b == -1 || c == -1 || d == -1) {
// 我曾经在这里吃过亏,输入里混了换行符
// 建议先做预处理,去掉空白字符
return {};
}
uint32_t val = (a << 18) | (b << 12) | (c << 6) | d;
result.push_back((val >> 16) & 0xFF);
result.push_back((val >> 8) & 0xFF);
result.push_back(val & 0xFF);
}
// 处理末尾的填充
if (padding > 0) {
size_t last_chunk = len - 4;
int a = kDecodeTable[(uint8_t)input[last_chunk]];
int b = kDecodeTable[(uint8_t)input[last_chunk+1]];
int c = (padding >= 2) ? 0 : kDecodeTable[(uint8_t)input[last_chunk+2]];
int d = (padding >= 1) ? 0 : kDecodeTable[(uint8_t)input[last_chunk+3]];
uint32_t val = (a << 18) | (b << 12) | (c << 6) | d;
result.push_back((val >> 16) & 0xFF);
if (padding < 2) result.push_back((val >> 8) & 0xFF);
if (padding < 1) result.push_back(val & 0xFF);
}
return result;
}
};
// 静态成员定义
const char Base64::kEncodeTable[65] =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
const int Base64::kDecodeTable[256] = {
-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,62,-1,-1,-1,63,
52,53,54,55,56,57,58,59,60,61,-1,-1,-1, 0,-1,-1,
-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13,14,
15,16,17,18,19,20,21,22,23,24,25,-1,-1,-1,-1,-1,
-1,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,
41,42,43,44,45,46,47,48,49,50,51,-1,-1,-1,-1,-1,
// 其余 128 个位置全部填 -1(省略)
};
小技巧:
解码时,我建议先对输入做一次“清洗”——去掉换行符、空格等空白字符。因为很多网络协议会在 Base64 字符串里插入换行,你不处理就会解码失败。
测试用例,跑一遍就放心
写完了代码,总得验证一下。我一般会写几个测试用例:
int main() {
// 测试 1:标准 3 字节
std::vector<uint8_t> data1 = {'M', 'a', 'n'};
std::string enc1 = Base64::Encode(data1);
std::cout << "Encode 'Man' = " << enc1 << std::endl; // 输出 TWFu
// 测试 2:1 字节,带填充
std::vector<uint8_t> data2 = {'M'};
std::string enc2 = Base64::Encode(data2);
std::cout << "Encode 'M' = " << enc2 << std::endl; // 输出 TQ==
// 测试 3:解码验证
auto dec1 = Base64::Decode("TWFu");
std::string str1(dec1.begin(), dec1.end());
std::cout << "Decode 'TWFu' = " << str1 << std::endl; // 输出 Man
// 测试 4:空数据
std::vector<uint8_t> empty;
std::string enc_empty = Base64::Encode(empty);
std::cout << "Encode empty = '" << enc_empty << "'" << std::endl;
return 0;
}
注意:
我曾经在解码时忘记处理 '=' 填充,结果最后几个字节全乱了。调试了半天才发现是填充逻辑写错了。所以,填充处理一定要仔细,尤其是末尾字节数不足的情况。
性能优化,能快一点是一点
如果你要处理大文件(比如几百 MB 的图片),逐字节编解码可能有点慢。我分享几个优化思路:
- 批量处理:一次读 3KB 数据,编解码完再写回去,减少函数调用开销。
- 查表法加速:解码时用 256 大小的数组直接查,比 switch-case 快 3~5 倍。
- SIMD 指令:如果平台支持,可以用 SSE/AVX 一次处理 16 字节,吞吐量能翻好几倍。
- 内存复用:避免频繁分配
std::vector,用预分配缓冲区。
说白了,Base64 本身不复杂,但细节决定成败。尤其是填充和非法字符处理,稍不留神就踩坑。
嗯,这个工具写好了,以后处理邮件附件、图片传输、甚至 JWT Token 解析,都能直接拿来用。