仅头文件的实现(C++17)
encode 接受字母表和填充开关作为参数,所以同一个函数既能生成标准 Base64,也能生成 URL 安全的 Base64。decode 接受带填充和不带填充的输入,但会拒绝无效字符、位置不对的 =、不可能出现的长度以及不为零的填充位。
// base64.hpp
#pragma once
#include <array>
#include <cstdint>
#include <optional>
#include <string>
#include <string_view>
#include <vector>
namespace b64 {
inline constexpr std::string_view kStd =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
inline constexpr std::string_view kUrl =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
inline std::string encode(const void* data, std::size_t n,
std::string_view abc = kStd, bool pad = true) {
auto p = static_cast<const std::uint8_t*>(data);
std::string out;
out.reserve((n + 2) / 3 * 4);
std::size_t i = 0;
for (; i + 2 < n; i += 3) {
std::uint32_t v = p[i] << 16 | p[i + 1] << 8 | p[i + 2];
out += abc[v >> 18 & 63];
out += abc[v >> 12 & 63];
out += abc[v >> 6 & 63];
out += abc[v & 63];
}
if (i < n) {
std::uint32_t v = p[i] << 16 | (i + 1 < n ? p[i + 1] << 8 : 0);
out += abc[v >> 18 & 63];
out += abc[v >> 12 & 63];
if (i + 1 < n) out += abc[v >> 6 & 63];
if (pad) out.append(i + 1 < n ? 1 : 2, '=');
}
return out;
}
inline std::string encode(std::string_view s, std::string_view abc = kStd,
bool pad = true) {
return encode(s.data(), s.size(), abc, pad);
}
inline std::optional<std::vector<std::uint8_t>> decode(
std::string_view s, std::string_view abc = kStd) {
std::array<int, 256> val;
val.fill(-1);
for (int i = 0; i < 64; ++i) val[static_cast<unsigned char>(abc[i])] = i;
std::size_t len = s.size();
if (len % 4 == 0 && len > 0 && s[len - 1] == '=')
len -= (s[len - 2] == '=') ? 2 : 1;
if (len % 4 == 1) return std::nullopt;
if (s.size() != len && s.size() % 4 != 0) return std::nullopt;
std::vector<std::uint8_t> out;
out.reserve(len * 3 / 4);
std::uint32_t acc = 0;
int bits = 0;
for (std::size_t i = 0; i < len; ++i) {
int v = val[static_cast<unsigned char>(s[i])];
if (v < 0) return std::nullopt;
acc = (acc << 6) | static_cast<std::uint32_t>(v);
bits += 6;
if (bits >= 8) {
bits -= 8;
out.push_back(static_cast<std::uint8_t>(acc >> bits));
acc &= (1u << bits) - 1;
}
}
if (acc != 0) return std::nullopt; // leftover bits must be zero
return out;
}
} // namespace b64
解码器以比特累加器的方式工作:每个字符加入 6 位,每凑满 8 位就输出一个字节。最后剩下的就是填充位,它们必须为零。原因见 Base64 的工作原理。
编码和解码字符串
#include "base64.hpp"
#include <iostream>
int main() {
std::string enc = b64::encode("héllo wörld");
std::cout << enc << '\n'; // aMOpbGxvIHfDtnJsZA==
if (auto bytes = b64::decode(enc))
std::cout << std::string(bytes->begin(), bytes->end()) << '\n'; // héllo wörld
}
std::string 存放的是字节。源文件为 UTF-8 时(GCC 和 Clang 的默认设置;MSVC 需要加 /utf-8),字面量就是 UTF-8,输出与其他所有语言一致。对于 C++20 的 std::u8string,把 s.data() 和 s.size() 传给指针版本的重载即可。
URL 安全与无填充
std::string q = "<<?\?\?>>"; // "<<???>>", escaped (see below)
b64::encode(q, b64::kUrl); // PDw_Pz8-Pg==
b64::encode(q, b64::kUrl, false); // PDw_Pz8-Pg
b64::decode("PDw_Pz8-Pg", b64::kUrl); // bytes of "<<???>>"
之所以要转义,是因为在 C++17 之前 ??> 是一个三字符组(trigraph);GCC 在严格的 -std=c++11 / c++14 模式下会替换它。JWT 和大多数令牌使用不带填充的 Base64URL,见 Base64 与 Base64URL 的区别。
校验结果
decode 的返回结果,均已验证:
| 输入 | 结果 |
|---|---|
aGk= | 2 个字节 |
aGk | 2 个字节(接受无填充) |
aA== / aA | 1 个字节 |
aGl= | nullopt(填充位不为零) |
aGk== | nullopt(长度错误) |
a=== | nullopt(填充过多) |
aGk=!! | nullopt |
PDw_Pz8-Pg== | 使用 kStd 时为 nullopt |
我们用 g++ -std=c++17 -Wall -Wextra -Wpedantic -fsanitize=address,undefined 构建,在两种字母表下对 5,000 个随机缓冲区做了往返测试,把编码器与 Python 的 b64encode 进行对比,并以 Python 参考实现为准,检查了 30,000 个随机字符串的接受或拒绝判断。没有任何不一致。想手动检查单个字符串,Base64 校验工具 会解释每次拒绝的原因。
文件
#include <fstream>
#include <iterator>
std::ifstream f("hello.txt", std::ios::binary);
std::string data((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
std::cout << b64::encode(data) << '\n'; // aGVsbG8K
auto raw = b64::decode("aGVsbG8K");
std::ofstream("copy.txt", std::ios::binary)
.write(reinterpret_cast<const char*>(raw->data()),
static_cast<std::streamsize>(raw->size()));
在 Windows 上 std::ios::binary 很重要,因为文本模式会转换换行符,从而损坏二进制数据。处理大文件时,每次读取大小为 3 的倍数的块,分别调用 encode,输出可以直接拼接。C 语言中的 Base64 页面给出了这个循环。如果要把结果转成 C 数组嵌入代码,可以用 Base64 转字节数组 工具生成。
你可能已经在用的库
如果下面某个库已经是项目依赖,用它的 Base64 API 就能少写一个文件:
| 库 | 编码 | 解码 |
|---|---|---|
| OpenSSL | EVP_EncodeBlock | EVP_DecodeBlock(返回的长度包含填充字节) |
| Abseil | absl::Base64Escape、absl::WebSafeBase64Escape | absl::Base64Unescape、absl::WebSafeBase64Unescape |
| Qt | QByteArray::toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals) | QByteArray::fromBase64 |
| Poco | Poco::Base64Encoder(一个 ostream) | Poco::Base64Decoder(一个 istream) |
| Boost | boost::beast::detail::base64(内部实现,不是稳定 API) | 同左 |
在依赖某个库做校验之前,先确认它如何处理填充和空白,各个库的行为并不相同。例如 EVP_DecodeBlock 不会从返回值中减去填充,所以 aGk= 报告的是 3 个字节,而不是 2 个。
常见陷阱
- 把有符号
char用作索引:当char为有符号类型时,对于大于 127 的字节,table[s[i]]是未定义行为。要像上面那样先转换为unsigned char。 - 把解码出的字节当作 C 字符串:解码出的字节可能包含
\0,千万不要把它们传给strlen,或任何只接受裸const char*、不带长度参数的 API。 - 把缓冲区大小算成
n * 4 / 3:这样最多会少 3 个字符。精确公式见 体积开销指南。
常见问题
C++ 有标准的 Base64 函数吗?
没有。标准库里没有,截至 C++23 合入的提案里也没有。可以用本页这样的小型实现,或者用项目已经依赖的某个库的 API。
decode 应该返回 std::string 还是 std::vector<uint8_t>?
Base64 解码出来的是二进制数据,所以 std::vector<std::uint8_t> 更如实地表达了这一点。如果你确定载荷是文本,再用 vector 的迭代器构造一个 std::string 即可。
为什么用 std::optional 而不是异常?
来自用户输入的无效 Base64 是预料之中的情况,并不算异常。std::optional 强制调用方处理它,而且在正常路径上没有任何开销。如果你的代码库偏好异常,可以在外面包一层再抛出。