必须只对非unreserved字符(a-z、a-z、0-9、-、.、_、~)编码,且用大写%xx格式;字节层面处理,%须编码为%25,已编码序列不应二次编码。

直接用 std::ostringstream 拼接或手写循环 + std::hex 很容易漏掉保留字符(如 /、?、:)的处理,导致编码不符合 RFC3986 —— 关键在于:**必须只对非“unreserved”字符进行编码,且大写字母形式(%2A 而非 %2a)**。
哪些字符需要编码?看 RFC3986 的 unreserved 集合
RFC3986 明确定义了不需编码的字符集(unreserved):A-Z、a-z、0-9、-、.、_、~。其余所有字节(包括空格、中文、控制字符、/、?、#、[、] 等)都必须百分比编码。
注意:+ 在 query string 中常被用作空格替代,但 RFC3986 并不认可这种用法;URL 路径或 fragment 中的空格必须编码为 %20,不是 +。
实操建议:
- 用查表法(256 字节布尔数组)判断是否需编码,O(1) 且无分支预测开销
- 预先初始化表:
need_encode[32] = true(空格),need_encode['/'] = true,need_encode['?'] = true,等等 - 特别注意:0x80–0xFF 字节(UTF-8 多字节首字节或后续字节)一律需编码,不要尝试 UTF-8 解码再判断 —— 编码操作应在字节层面进行
如何安全地把字节转成 %XX 格式(大写十六进制)
C++ 标准库默认 std::hex 输出小写,且不补零;直接用 std::setw(2) 容易因流状态污染影响后续输出。
更稳妥的做法是手动转换:
char hex_chars[] = "0123456789ABCDEF";
std::string result;
result.reserve(input.size() * 3); // 上限预分配
for (unsigned char c : input) {
if (need_encode[c]) {
result += '%';
result += hex_chars[c >> 4];
result += hex_chars[c & 0x0F];
} else {
result += c;
}
}
关键点:
- 用
unsigned char遍历,避免char为负时导致数组越界(如need_encode[-1]) - 硬编码大写字符表,不依赖 iostream 格式化,无状态副作用
-
reserve()避免多次内存重分配,尤其对长字符串明显提升性能
要不要对已编码的字符串二次编码?(常见误用场景)
如果输入字符串里已经含有类似 %20 的合法编码序列,**不应再对其中的 %、2、0 单独编码** —— 这属于双重编码(double-encoding),会导致 %20 变成 %2520,服务端无法正确解码。
但 RFC3986 并未要求自动识别并跳过已有 %XX 序列;是否跳过取决于你的使用场景:
- 若输入来自用户自由输入(如搜索关键词),应完全编码,不管它看起来像不像已编码内容
- 若输入是拼接生成的 URL 片段(比如你已确保某段是安全的 path),则需业务层自己保证不混入原始
%字符 - 没有通用“智能解码再编码”方案;想跳过已有
%XX,必须显式做三字符匹配(%+ 两个十六进制字符),且要验证十六进制有效性(否则%GG会被错误跳过)
最易被忽略的是:RFC3986 对百分号本身(%)的定义 —— 它属于 “sub-delimiter”,不在 unreserved 集合中,必须编码为 %25。这意味着,任何未经校验就插入 % 字符到 URL 中的行为,都会破坏合规性。别信“浏览器能容错”,服务端严格解析时会失败。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











