对于string中的每个char都会给出错误的结果

时间:2017-07-09 09:41:19

标签: c++ utf-8

有一个UTF-8编码的字符串,我可以从文件中读取它并将其写入另一个文件就好了。但是当我尝试逐个加载该字符串中的每个字符时,结果并不是连贯的。我很可能以非常错误的方式做这件事,但这样做的正确方法是什么?

source.txt中的内容

afternoon_gb_1          ɑftənun

我写的代码是

while (source >> word >> word_ipa) { 
for (char& c : word_ipa)
 myfile <<word<<" is " << c<< endl;}

txt文件myfile中的内容被写为

afternoon_gb_1 is �
afternoon_gb_1 is �
afternoon_gb_1 is f
afternoon_gb_1 is t
afternoon_gb_1 is �
afternoon_gb_1 is �
afternoon_gb_1 is n
afternoon_gb_1 is u
afternoon_gb_1 is n

1 个答案:

答案 0 :(得分:4)

在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:代码点编号; UTF-8中的c.p.代码单位表示; 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序列都遵循这种模式。