字符串的 Base64 编码:原理与 C 语言实现
Base64 编码,说白了就是把二进制数据转成可见字符。我早年做网络协议开发时,经常要处理邮件附件和图片传输,那时候 Base64 几乎是绕不开的坎。你想想看,有些协议只认文本,不认二进制,怎么办?Base64 就是那个「翻译官」。
Base64 编码原理
Base64 的核心思想很简单:用 64 个可打印字符来表示任意二进制数据。这 64 个字符包括 A-Z、a-z、0-9、+ 和 /,再加上一个填充符 =。
具体怎么做的呢?我一步步拆给你看:
- 分组:每 3 个字节(24 位)为一组
- 拆分:把这 24 位分成 4 个 6 位的片段
- 映射:每个 6 位值(0-63)对应一个 Base64 字符
- 填充:如果最后不足 3 字节,用 = 补齐
举个例子,字符串 "Man" 的编码过程:
原始数据: M a n
ASCII: 0x4D 0x61 0x6E
二进制: 01001101 01100001 01101110
分组: 010011 010110 000101 101110
十进制: 19 22 5 46
Base64: T W F u
结果就是 "TWFu"。你看,3 个字节变成了 4 个字符,体积膨胀了约 33%。
关键点:Base64 不是加密,只是编码。它不保证数据安全,只保证数据能在文本通道中传输。
Base64 字符表
这是那 64 个字符的映射表,我建议你把它记下来,或者至少知道去哪查:
| 值 | 字符 | 值 | 字符 | 值 | 字符 | 值 | 字符 |
|---|---|---|---|---|---|---|---|
| 0 | A | 16 | Q | 32 | g | 48 | w |
| 1 | B | 17 | R | 33 | h | 49 | x |
| 2 | C | 18 | S | 34 | i | 50 | y |
| 3 | D | 19 | T | 35 | j | 51 | z |
| 4 | E | 20 | U | 36 | k | 52 | 0 |
| 5 | F | 21 | V | 37 | l | 53 | 1 |
| 6 | G | 22 | W | 38 | m | 54 | 2 |
| 7 | H | 23 | X | 39 | n | 55 | 3 |
| 8 | I | 24 | Y | 40 | o | 56 | 4 |
| 9 | J | 25 | Z | 41 | p | 57 | 5 |
| 10 | K | 26 | a | 42 | q | 58 | 6 |
| 11 | L | 27 | b | 43 | r | 59 | 7 |
| 12 | M | 28 | c | 44 | s | 60 | 8 |
| 13 | N | 29 | d | 45 | t | 61 | 9 |
| 14 | O | 30 | e | 46 | u | 62 | + |
| 15 | P | 31 | f | 47 | v | 63 | / |
Base64 编码流程图
我画了一张图,帮你把整个流程串起来:
C 语言实现
好了,原理讲完了,咱们直接上代码。我习惯把编码和解码分开写,这样逻辑更清晰。
编码函数
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
// Base64 字符表
static const char base64_table[] =
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz"
"0123456789+/";
// 编码函数
char* base64_encode(const unsigned char* input, size_t len) {
// 计算输出长度
size_t output_len = 4 * ((len + 2) / 3);
char* output = (char*)malloc(output_len + 1);
if (!output) return NULL;
size_t i, j = 0;
for (i = 0; i < len; i += 3) {
unsigned int val = 0;
int remaining = len - i;
// 取3个字节
val |= (i < len) ? (input[i] << 16) : 0;
val |= (i + 1 < len) ? (input[i + 1] << 8) : 0;
val |= (i + 2 < len) ? input[i + 2] : 0;
// 生成4个Base64字符
output[j++] = base64_table[(val >> 18) & 0x3F];
output[j++] = base64_table[(val >> 12) & 0x3F];
if (remaining > 1) {
output[j++] = base64_table[(val >> 6) & 0x3F];
} else {
output[j++] = '=';
}
if (remaining > 2) {
output[j++] = base64_table[val & 0x3F];
} else {
output[j++] = '=';
}
}
output[j] = '\0';
return output;
}
我的经验:编码时最容易出错的地方是填充处理。我曾经在嵌入式项目里忘了处理剩余字节,结果解码出来的图片全是花屏。后来我养成了一个习惯——编码完立刻用已知数据做一次解码验证。
解码函数
// 反向查找字符在表中的位置
static int base64_index(char c) {
if (c >= 'A' && c <= 'Z') return c - 'A';
if (c >= 'a' && c <= 'z') return c - 'a' + 26;
if (c >= '0' && c <= '9') return c - '0' + 52;
if (c == '+') return 62;
if (c == '/') return 63;
return -1; // 非法字符
}
// 解码函数
unsigned char* base64_decode(const char* input, size_t* out_len) {
size_t len = strlen(input);
if (len % 4 != 0) return NULL; // 非法长度
// 计算填充字符数
size_t padding = 0;
if (len > 0 && input[len - 1] == '=') padding++;
if (len > 1 && input[len - 2] == '=') padding++;
// 计算输出长度
*out_len = (len / 4) * 3 - padding;
unsigned char* output = (unsigned char*)malloc(*out_len);
if (!output) return NULL;
size_t i, j = 0;
for (i = 0; i < len; i += 4) {
unsigned int val = 0;
int valid_chars = 4;
// 处理填充
for (int k = 0; k < 4; k++) {
if (input[i + k] == '=') {
valid_chars = k;
break;
}
int idx = base64_index(input[i + k]);
if (idx == -1) {
free(output);
return NULL; // 非法字符
}
val = (val << 6) | idx;
}
// 提取字节
if (valid_chars > 1) output[j++] = (val >> 16) & 0xFF;
if (valid_chars > 2) output[j++] = (val >> 8) & 0xFF;
if (valid_chars > 3) output[j++] = val & 0xFF;
}
return output;
}
使用示例
int main() {
const char* test = "Hello, Base64!";
size_t len = strlen(test);
// 编码
char* encoded = base64_encode((unsigned char*)test, len);
printf("原始数据: %s\n", test);
printf("编码结果: %s\n", encoded);
// 解码
size_t decoded_len;
unsigned char* decoded = base64_decode(encoded, &decoded_len);
printf("解码结果: %.*s\n", (int)decoded_len, decoded);
free(encoded);
free(decoded);
return 0;
}
运行结果:
原始数据: Hello, Base64!
编码结果: SGVsbG8sIEJhc2U2NCE=
解码结果: Hello, Base64!
注意:解码时一定要检查输入长度是否为 4 的倍数,否则直接返回 NULL。我曾经在项目中遇到过有人传了截断的 Base64 字符串,结果解码函数直接崩溃了。加个长度校验,能省很多调试时间。
常见问题与避坑
- 字符集问题:标准 Base64 用 + 和 /,但 URL 中这两个字符有特殊含义。我建议在 URL 场景下改用 - 和 _,这就是所谓的 URL-safe Base64。
- 内存管理:编码和解码都动态分配了内存,调用者一定要记得 free。我在嵌入式项目里吃过内存泄漏的亏,后来统一用工具检查。
- 性能优化:如果处理大量数据,可以考虑查表法代替计算。我做过测试,查表法比移位运算快大约 30%。
一句话总结:Base64 就是用 64 个字符表示二进制数据,编码膨胀 33%,解码时注意填充和非法字符。写代码时多想想边界情况,尤其是最后那几字节的处理。