简而言之
在索引之前,您需要对UTF-8进行解码。请继续阅读以获取比我预期要写的更多细节...
C++流不具备编码感知能力 - 它只是一串字节。例如,以下代码可以成功地转储整个UTF-8字符串:
#include <iostream>
#include <sstream>
#include <string>
int main() {
std::stringstream instream;
instream << u8"█\n \n▀\n▄\n▓\n";
std::cout << instream.rdbuf();
}
[1]: https://en.cppreference.com/w/cpp/language/string_literal
你的问题源于 UTF-8 编码本身。UTF-8 是一种多字节编码。一些字符(特别是 ASCII 字符)被编码为单个字节。例如,字母 a
被编码为值 97(十六进制中的 0x61
)。
让我们来看看你尝试打印的五个字符:
字符 |
Unicode 代码点 |
UTF-8 编码 |
Unicode 名称 |
█ |
U+2588 |
0xe2 0x96 0x88 |
全块 |
|
U+20 |
0x20 |
空格 (无链接;这只是普通的 ASCII 字符) |
▀ |
U+2580 |
0xe2 0x96 0x80 |
上半块 |
▄ |
U+2584 |
0xe2 0x96 0x84 |
下半块 |
▓ |
U+2593 |
0xe2 0x96 0x93 |
暗阴影 |
UTF-8编码是这里的重点 - 这就是每个字符在UTF-8编码文件中存储为一系列字节的方式。对于四个块绘图字符(我们将忽略空格,因为那只是一个单字节字符),编码需要三个字节。
“但是,如果代码点只有两个字节长,为什么编码要占用三个字节呢?”
好问题。让我们分解第一个字符:
0xe2 0x96 0x88
11100010 10010110 10001000
AAAA^^^^ BB^^^^^^ BB^^^^^^
二进制下方的注释说明了编码的工作原理。
由于字符的代码点太大,无法适应单个字节,因此UTF-8将其分成多个字节。然而,必须有一种方法来确定一个字节序列表示一个字符,而不仅仅是一系列简单的字符。这就是字节前缀(A、B和C)发挥作用的地方。多字节序列中的第一个字节以
1
位序列开头,表示编码字符的总字节数,后跟终止符
0
。在这里,我们需要三个字节,所以我们有
1110
(A)。
其余两个字节的前缀表示它们是连续字节(即它们不应被视为字符的开始)。连续字节的前缀定义为
10
(B)。
在删除这些前缀之后,剩余的位(用插入符号[
^
]标记)被打包和解析以检索编码的代码点。
单字节字符(即从0到127的基本US-ASCII字符平面)只需要7位来编码,因此前缀
0
表示此字符没有连续字节。
这一切与你的问题有什么关系?
我之前说过,“你的问题源自于UTF-8编码本身”,但其实我撒了谎,抱歉。你的问题来自于试图将UTF-8编码的数据作为普通字节序列进行读取。
根据上面的编码表,让我们来看看你文件中的原始字节(假设每行以单个\n
结束):
e2 96 88 0a 20 0a e2 96 80 0a e2 96 84 0a e2 96 93 0a
\--01--/ 02 \--03--/ \--04--/ \--05--/
我已经按照行号标记了字符。
从这个转储中,你可以轻易地看到你有问题的代码的输出结果:
char displayCharacters[5] = {};
std::cout << "Here is the first symbol: " << displayCharacters[4];
这是一个空格!请记住,流不知道文件的编码方式,因此它只会输出一系列字节(在C/C++中,char
只是一个8位变量)。您的数组(displayCharacters
)包含上面显示的字节序列,因此将其作为下标取第四个(从零开始)元素将返回字节0x20
。
在这里,您实际上很幸运。将UTF-8数据索引为原始字节通常会导致更丑陋的错误。还记得那些连续字节(以10
开头)吗?如果您提取并尝试单独打印其中之一,则终端将不知道该怎么处理它。同样,对于多字节序列的开头(前缀11
)也是如此。
适当地索引UTF-8字符串非常困难。您几乎肯定需要使用库来处理它。
根据所涉及文件的用途和/或来源,您可能需要考虑使用固定宽度编码,例如UTF-32。