直接用substr截UTF-8字符串会出错,因为substr按字节截取而UTF-8是变长编码,中文、emoji等多字节字符被截断中间字节会导致乱码;需按Unicode字符边界安全截取。

为什么直接用 substr 截 UTF-8 字符串会出错
因为 substr 按字节截取,而 UTF-8 是变长编码:ASCII 字符占 1 字节,中文通常占 3 字节,emoji 可能占 4 字节。直接截断中间字节会导致乱码(如显示 )或解析失败。
典型错误现象:"你好世界".substr(0, 4) 可能返回 "你" —— 因为前两个汉字共 6 字节,截 4 字节刚好卡在第二个汉字的中间。
- UTF-8 单个字符的字节长度由首字节高位模式决定:
0xxxxxxx(1 字节)、110xxxxx(2 字节)、1110xxxx(3 字节)、11110xxx(4 字节) -
std::string::size()返回的是字节数,不是字符数 - 没有标准库函数能直接按“Unicode 字符”截取
std::string
手动识别 UTF-8 起始字节实现安全截取
核心思路:从字符串开头逐字节扫描,跳过续字节(10xxxxxx),只把起始字节当作一个字符起点;累计到目标字符数后,用该位置作为 substr 的截止点。
示例逻辑(C++17):
立即学习“C++免费学习笔记(深入)”;
组合式C++代码评审方案,融合静态分析、AI推理、多轮迭代评审和C++专项检查,适用于PR审查、增量代码审查、全项目评审和代码质量评分,触发词包括review cpp、cpp代码评审、C++review、代码审查。
size_t utf8_truncate(const std::string& s, size_t max_chars) {
size_t pos = 0;
size_t chars = 0;
while (pos < s.size() && chars < max_chars) {
unsigned char c = s[pos];
if ((c & 0b10000000) == 0) { // 1-byte: 0xxxxxxx
pos += 1;
} else if ((c & 0b11100000) == 0b11000000) { // 2-byte
pos += 2;
} else if ((c & 0b11110000) == 0b11100000) { // 3-byte
pos += 3;
} else if ((c & 0b11111000) == 0b11110000) { // 4-byte
pos += 4;
} else { // invalid or continuation byte → treat as 1 byte (fallback)
pos += 1;
}
++chars;
}
return pos; // 字节偏移,可传给 substr
}
- 返回的是字节位置,不是字符数,直接用于
s.substr(0, utf8_truncate(s, 5)) - 对非法 UTF-8 字节(如单独出现
10xxxxxx)做容错:当字节不匹配任何起始模式时,按 1 字节前进 - 性能尚可,O(n) 单次遍历;若频繁截取,建议缓存字符边界索引
使用 ICU 或 utf8cpp 库的更健壮方案
手写逻辑容易漏掉边界情况(如超长编码、代理对、BOM 处理)。生产环境推荐成熟库:
-
utf8cpp(header-only):utf8::distance(s.begin(), s.end())得字符总数;utf8::next(s.begin(), s.end())迭代每个字符起始位置 - ICU(重量级但完整):
icu::UnicodeString::moveFrom()+char32_t转换,支持 Unicode 标准化和图形簇(grapheme cluster)截取 - 注意:
std::wstring_convert已被 C++17 弃用,不要用
轻量场景推荐 utf8cpp,只需引入 utf8.h,示例:
#include <utf8.h>
std::string safe_substring(const std::string& s, size_t max_chars) {
auto begin = s.begin();
auto end = s.begin();
for (size_t i = 0; i < max_chars && end != s.end(); ++i) {
end = utf8::next(end, s.end());
}
return std::string(begin, end);
}
截取后要不要加省略号?注意字节 vs 字符长度
加 "…" 时,必须把它当作 UTF-8 字符处理:3 字节,不能简单拼接导致总长度超标。
- 错误做法:
s.substr(0, max_bytes) + "..."——"..."是 ASCII,但若原截取已满字节上限,拼接后可能超限 - 正确做法:先预留 3 字节空间给省略号,再截取,最后拼接:
safe_substring(s, max_chars - 1) + "…" - 如果
max_chars == 0,直接返回空串;如果max_chars == 1,省略号本身占 1 字符,就不能再截原文了
最易被忽略的是:同一个视觉字符(如带变音符号的 é)可能由多个 Unicode 码点组成,严格按“用户感知字符”截取需 grapheme cluster 分析——这已超出纯 UTF-8 字节解析范畴,得靠 ICU。

















