如何将非ASCII字符导入到控制台?

4

我对这个问题思考了一段时间,现在需要一些帮助。基本上我想让代码读取一系列非ASCII符号到一个预设的空数组中,并将它们打印出来以查看是否已被读入,但目前并没有。记事本可以正常显示它们,但由于某种原因C++不能识别它们为有效字符,强烈建议只提供关于代码的建议而不是更改我的计算机内部设置。

char displayCharacters[5] = {};

try {

    instream.open("characters.txt");
    instream >> displayCharacters;
    cout << "Here is the first symbol: " << displayCharacters[4];

} 

catch (exception) {

    cout << "Something went wrong with the file handling.";

}

是的,我已经正确设置了输入流,包括使用iostream库和using namespace std语句。以下是文件内容:

█
 
▀
▄
▓

编辑:如果您需要知道,该文件是UTF-8编码。


如果您的文件不是ASCII格式,它使用的是什么编码?(我假设是UTF-8,因为这些字符在我的浏览器中呈现正常) - MTCoster
1
是的,它是UTF-8编码,可能应该澄清一下。让我编辑一下。 - Kitso
你的输入文件里有什么?能否提供一个可以编译的程序而不是一小段代码片段? - Caleb
1
使用一些UTF-8库,例如GNU libunistringQtPOCO - Basile Starynkevitch
我已经将文件内容包含在问题本身中了,也许有预装的库可以使用,还是需要找到您列出的其中一个库? - Kitso
1个回答

8

简而言之

在索引之前,您需要对UTF-8进行解码。请继续阅读以获取比我预期要写的更多细节...


C++流不具备编码感知能力 - 它只是一串字节。例如,以下代码可以成功地转储整个UTF-8字符串:

#include <iostream>
#include <sstream>
#include <string>

int main() {
    // Simulate your `instream` using an `std::stringstream`
    std::stringstream instream;
    // Load the simulated `instream` using a UTF-8 string literal [1]
    instream << u8"█\n \n▀\n▄\n▓\n";
    
    // Print entire `instream`
    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


在这种情况下,您是否有任何推荐的库?如何在不改变计算机的基本设置的情况下更改编码方式?所有这些信息都是相当有教育意义的,我很感激。 - Kitso
Basile在你的问题下方的建议都非常好。如果您已经在项目中使用其他Boost组件,我会将Boost.Locale加入其中。 - MTCoster
我已经看过它们了,虽然我认为它们都很好,但我主要的问题是它们都是需要下载的外部库。我正试图使用基本的C++软件包来完成这个任务,因为我有点担心在安装这些库时可能会出现更多错误。 - Kitso
@Kitso,如果你尝试自己编写代码而不是链接一个广泛使用的已建立库,那么你更有可能引入错误。 - MTCoster

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接