在 UTF-8 中,每个代码点(=逻辑字符)由多个代码单元(=char)表示; ɑftənun,尤其是:
ch| c.p. | c.u.
--+------+-------
ɑ | 0251 | c9 91
f | 0066 | 66
t | 0074 | 74
ə | 0259 | c9 99
n | 006e | 6e
u | 0075 | 75
n | 006e | 6e
(ch=character;c.p.:码位编号;c.p. UTF-8 编码单元表示;c.u. 和 c.p. 以十六进制表示)
解释了代码点如何映射到代码单元的确切细节in many places;最基本的是:
- 小于 0x7f 的代码点直接映射到单个代码单元;对于这些,永远不会设置高位;
- 从 0x80 开始的代码点被映射到多个代码单元;多代码单元序列中的所有代码单元都设置了高位;
- 如果设置了高位,则高位具有特定含义;在多字节序列的第一个字节中,它们告诉预期有多少个连续字节,在其他字节中,它们被明确标记为连续字节。
如果您单独打印出每个代码单元,您将破坏需要表示多个代码单元的代码点的 UTF-8 编码。您的终端应用程序在第一行看到
c9 0a
(第一个代码单元后跟换行符),并立即检测到这是一个损坏的 UTF-8 序列,因为 c9 设置了高位但下一个 c.u.没有;因此是 � 字符。第二个字符以及 c.u. 也是如此。表示ə的部分序列。
现在,如果你想打印出完整的代码点(不是代码单元),std::string 不会有任何帮助 - std::string 对这些东西一无所知,它本质上是一个美化的std::vector<char>,完全忽略了编码问题;它所做的只是存储/索引代码单元,而不是代码点。
但是有第三方库可以帮助解决这个问题; utf8-cpp 是一个小而完整的;在您的情况下,utf8::next 函数会特别有用:
while (source >> word >> word_ipa) {
auto cur = word_ipa.begin();
auto end = word_ipa.end();
auto next = cur;
for(;cur!=end; cur=next) {
utf8::next(next, end);
myfile << word << "is ";
for(; cur!=next; ++cur) myfile<<*cur;
myfile << "\n";
}
}
utf8::next 这里只是增加给定的迭代器,使其指向开始下一个代码单元的代码点;此代码确保我们将构成单个代码点的所有代码单元打印在一起。
请注意,我们可以非常简单地重现其准系统行为,只需阅读 UTF-8 规范(请参阅上面维基百科链接中的第一个表格):
template<typename ItT>
void safe_advance(ItT &it, size_t n, ItT end) {
size_t d = std::distance(it, end);
if(n>d) throw std::logic_error("Truncated UTF-8 sequence");
std::advance(it, n);
}
template<typename ItT>
void my_next(ItT &it, ItT end) {
uint8_t b = *it;
if(b>>7 == 0) safe_advance(it, 1, end);
else if(b>>5 == 6) safe_advance(it, 2, end);
else if(b>>4 == 14) safe_advance(it, 3, end);
else if(b>>3 == 30) safe_advance(it, 4, end);
else throw std::logic_error("Invalid UTF-8 sequence");
}
这里我们利用了这样一个事实,即序列的第一个字节声明了要完成多少个额外的代码点来完成代码单元。
(请注意,这需要有效的 UTF-8,并且不会尝试重新同步损坏的 UTF-8 序列;库版本在这方面可能要好得多)
OTOH,也可以内联将相同代码单元保持在一起所必需的内容:
while (source >> word >> word_ipa) {
auto cur = word_ipa.begin();
auto end = word_ipa.end();
for(;cur!=end;) {
myfile << word << "is "<<*cur;
if(uint8_t(*cur++)>>7 != 0) {
for(; cur!=end && (uint8_t(*cur)>>6)==2; ++cur) myfile<<*cur;
}
myfile << "\n";
}
}
在这里,我们完全忽略了第一个 c.u. 中的“声明计数”,我们只检查高位是否设置;在这种情况下,只要我们得到 c.u.,我们就会继续打印。前两个字节设置为 10(二进制,AKA 十进制 2) - 因为“延续 c.u.”多 c.u. UTF-8 序列都遵循这种模式。