rfc 3986规定必须编码的字符是除a-z、a-z、0-9、-、.、_、~外的所有字节,包括空格(%20)、保留字符(如/、?、#等非结构用途时)、中文及emoji等utf-8多字节字符,且需按字节级逐个编码。

哪些字符必须被编码?RFC3986 的核心规则
RFC3986 定义了「子分隔符」和「未保留字符」的边界。真正需要编码的,是所有非 unreserved 字符:即除了 A-Z、a-z、0-9、-、.、_、~ 之外的任意字节(注意:不是 Unicode 字符,而是 UTF-8 编码后的每个字节)。空格必须编码为 %20,不能用 +(那是 application/x-www-form-urlencoded 的规则,不适用于通用 URI)。
-
/、?、#、[、]等在路径或查询中可能有语法意义,若作为数据出现就必须编码 - 中文、 emoji 等会以 UTF-8 多字节形式存在,每个字节单独判断并编码(例如
中→E4 B8 AD→%E4%B8%AD) - 不要对已存在的
%XX序列二次编码(除非你明确要双重编码),否则会导致%2520这类错误
手写编码函数时怎么处理字节与十六进制转换?
别依赖 std::hex 或流式格式化——它们不保证两位补零且易受 locale 影响。直接用查表或位运算更可靠、更可控:
- 建立静态 const char 数组:
static constexpr char HEX_DIGITS[] = "0123456789ABCDEF"; - 对每个字节
b,用HEX_DIGITS[b >> 4]和HEX_DIGITS[b & 0x0F]拼出两个十六进制字符 - 使用
std::string::reserve()预估容量(最坏情况:原始字符串长度 × 3)避免多次 realloc - 判断是否需编码时,用查表布尔数组(256 元素)比每次调用
isalnum()+ 多个||更快也更准确(后者不处理 ASCII 以外字节)
bool should_encode(unsigned char c) {
static const bool lookup[256] = {
[0 ... 32] = true, // control chars
['A'...'Z'] = false, ['a'...'z'] = false,
['0'...'9'] = false, ['-'] = false, ['.'] = false,
['_'] = false, ['~'] = false,
[127 ... 255] = true
};
return lookup[c];
}
std::string_view 能否安全用于输入?
可以,但要注意:
-
std::string_view不拥有数据,确保其指向的内存生命周期长于编码函数执行时间 - 如果输入是 UTF-8 字符串,
string_view的data()和size()直接对应字节序列,无需额外转换 - 不要用
string_view的begin()/end()遍历「字符」——那会按 char 遍历,正好符合 RFC3986 要求的字节级编码 - 若传入的是
std::u8string(C++20),其data()也是 UTF-8 字节流,可直接用
常见错误:混淆 URI 组件编码与整个 URL 编码
URL 百分比编码必须按组件分别进行,不能对完整 URL 字符串整体调用一次编码函数:
- 路径中的
/是分隔符,不应编码;但作为路径段内容(如文件名含/)时必须编码为%2F - 查询参数值里的
&、=必须编码;但查询字符串本身的分隔符不能动 - 最稳妥做法:只对「数据部分」编码(比如文件名、搜索关键词、JSON 值),再拼入 URI 模板
实际中容易漏掉的点:
- 把
std::to_string()结果直接拼进 query,却忘了对其中可能含有的特殊字符(如用户输入的foo bar)做编码 - 用
curl_easy_escape()或其他 C 库时,没检查它是否默认启用curl的 legacy mode(可能把~也编码了) - 在 Windows 上读取文件名后未转为 UTF-8,导致非 ASCII 字节被误判或损坏
编码本身不难,难的是始终清楚「这个字节此刻是不是 URI 数据的一部分」。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











