必须手动校验utf-8字节序列合法性,因std::codecvt_utf8已弃用、iconv不校验且行为不可控;需按rfc 3629逐字节状态机判断起始字节类型、续字节范围、超长编码、代理对、截断等非法情形。

直接遍历字节做状态机校验,别依赖 std::codecvt_utf8 或 iconv —— 前者在 C++17 已弃用,后者只转码不校验,且行为不可控。
为什么必须手动校验字节序列
UTF-8 合法性 ≠ 能解码成功。RFC 3629 定义的是字节组合规则,不是 Unicode 映射结果。一个字符串可能被 std::wstring_convert 悄悄截断或替换为 ,但你根本不知道它从哪开始非法。常见误判包括:
- 把
0xC0 0x80当成合法两字节序列(实际是超长编码,禁止) - 忽略
0xED 0xA0开头的三字节序列(代理对,Unicode 禁止) - 没检查末尾是否截断(如
0xE0 0x80只有两字节,但0xE0要求跟两个续字节) - 用
char直接位运算,导致0x80–0xFF被解释为负数,b & 0xE0结果错乱
逐字节状态机的关键判断点
每个字节按起始类型分四类处理,所有分支都必须显式转为 unsigned char:
-
ASCII(1 字节):
b → 合法,跳过 -
两字节起始:
(b & 0xE0) == 0xC0→ 排除b == 0xC0 || b == 0xC1;再检查下一位是否存在且(s[i+1] & 0xC0) == 0x80 -
三字节起始:
(b & 0xF0) == 0xE0→ 检查后两位均满足续字节条件;额外排除b == 0xED && (s[i+1] & 0xF0) == 0xA0(代理对) -
四字节起始:
(b & 0xF8) == 0xF0→ 要求后三位合法;排除b >= 0xF5(超出U+10FFFF) - 任何单独出现的
0x80–0xBF→ 孤立尾字节,非法
std::string 使用时的边界陷阱
std::string 允许嵌入 \0,但 UTF-8 中 U+0000 是合法字符,不能因遇到 \0 就停;同时需注意:
- 空字符串返回
true(RFC 允许) - 遍历时用索引
i而非范围 for,避免auto c : s的符号扩展问题 - 末尾剩余字节数不足完整编码长度(如只剩 1 字节却期望是
0xE0开头)→ 直接判非法 - 采样长度建议设为
min(s.size(), 4096U),全量扫描无必要且低效
文件级校验要先跳过 BOM
读文件时必须用二进制模式,前 4 字节优先检查 BOM:
-
0xEF 0xBB 0xBF→ UTF-8 BOM,验证从第 4 字节开始 -
0xFF 0xFE或0xFE 0xFF→ 不是 UTF-8,可直接返回false - 读不满 3 字节(如空文件、仅 1–2 字节)→ 按“非 UTF-8”保守处理
- BOM 不是强制项,没 BOM 的 UTF-8 文件完全合法,BOM 只是线索,不能反向推断
真正难的不是写循环,而是记住哪些字节永远非法:比如 0xC0/0xC1、0xF5–0xFF、孤立的 0x80–0xBF,以及所有续字节不在 0x80–0xBF 范围的位置——漏检一个,整个判断就失去意义。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











