在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=字符; c.p.: 代码点数; c.p.代码单元在UTF-8中的表示方式; c.u.和c.p.用十六进制表示)
如何将代码点映射到代码单元的确切细节在许多地方有解释;基本上是:
- 小于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序列;库版本在这方面可能表现更好)
另一方面,也可以内联只需保持相同代码单元所需的内容:
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.中的“声明计数”,我们只检查高位是否设置;在这种情况下,只要我们得到顶部两个字节设置为10(二进制中称为2),就继续打印 - 因为多个c.u. UTF-8序列的“连续c.u.”都遵循此模式。