字符串的 Base64 编码:原理与 C 语言实现

Base64 编码,说白了就是把二进制数据转成可见字符。我早年做网络协议开发时,经常要处理邮件附件和图片传输,那时候 Base64 几乎是绕不开的坎。你想想看,有些协议只认文本,不认二进制,怎么办?Base64 就是那个「翻译官」。

Base64 编码原理

Base64 的核心思想很简单:用 64 个可打印字符来表示任意二进制数据。这 64 个字符包括 A-Z、a-z、0-9、+ 和 /,再加上一个填充符 =。

具体怎么做的呢?我一步步拆给你看:

  1. 分组:每 3 个字节(24 位)为一组
  2. 拆分:把这 24 位分成 4 个 6 位的片段
  3. 映射:每个 6 位值(0-63)对应一个 Base64 字符
  4. 填充:如果最后不足 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 个字符的映射表,我建议你把它记下来,或者至少知道去哪查:

字符字符字符字符
0A16Q32g48w
1B17R33h49x
2C18S34i50y
3D19T35j51z
4E20U36k520
5F21V37l531
6G22W38m542
7H23X39n553
8I24Y40o564
9J25Z41p575
10K26a42q586
11L27b43r597
12M28c44s608
13N29d45t619
14O30e46u62+
15P31f47v63/

Base64 编码流程图

我画了一张图,帮你把整个流程串起来:

Base64 编码流程 输入二进制数据 步骤1:每3字节(24位)为一组 步骤2:拆分为4个6位片段 步骤3:每个6位值映射为Base64字符 输出Base64字符串 特殊情况处理 • 剩余1字节:补2个= • 剩余2字节:补1个= • 完整3字节:不补 填充字符 = 不是数据

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%,解码时注意填充和非法字符。写代码时多想想边界情况,尤其是最后那几字节的处理。

公众号:蓝海资料掘金营,微信deep3321