为什么扩展的 ASCII(特殊)字符需要 2 个字节才能存储



ASCII 的范围从 32 到 126 是可打印的。 127 是DEL,此后被视为扩展字符。

为了检查它们是如何存储在std::string中的,我编写了一个测试程序:

int main ()
{
  string s; // ASCII
  s += "!"; // 33
  s += "A"; // 65
  s += "a"; // 97
  s += "â"; // 131
  s += "ä"; // 132
  s += "à"; // 133
  cout << s << endl;  // Print directly
  for(auto i : s)     // Print after iteration
    cout << i;
  cout << "ns.size() = " << s.size() << endl; // outputs 9!
}

上面代码中可见的特殊字符实际上看起来不同,这些字符可以在此在线示例中看到(在 vi 中也可见)。

在字符串s中,前 3 个普通字符按预期各获取 1 个字节。接下来的 3 个扩展字符每个占用 2 个字节。

问题

  1. 尽管是 ASCII(在 0 到 256 的范围内),为什么这 3 个扩展字符占用 2 个字节的空间?
  2. 当我们使用基于范围的循环遍历s时,如何确定普通字符必须增加 1 倍,扩展字符必须增加 2 倍!?

[注意:这也可能适用于 C 语言和其他语言。

  1. 尽管是 ASCII(在 0 到 256 的范围内),为什么这 3 个扩展字符占用 2 个字节的空间?
如果您将"be ASCII"定义为仅包含范围 [0, 256) 中的字节,则所有数据都是 ASCII

:[0, 256) 与字节能够表示的范围相同,因此根据您的定义,所有用字节表示的数据都是 ASCII。

问题是您的定义不正确,并且您错误地查看了数据类型的确定方式;由字节序列表示的数据类型不是由这些字节确定的。相反,数据类型是字节序列外部的元数据。(这并不是说不可能检查字节序列并从统计上确定它可能是哪种数据。

让我们检查一下您的代码,牢记上述内容。我从源代码的两个版本中获取了相关片段:

s += "â"; // 131
s += "ä"; // 132
s += "â"; // 131
s += "ä"; // 132

您正在将这些源代码片段视为在浏览器中呈现的文本,而不是原始二进制数据。您将这两件事呈现为"相同"的数据,但实际上它们并不相同。上图是两个不同的字符序列。

然而,这两个文本元素序列有一些有趣的东西:其中一个,当使用某种编码方案编码为字节时,当另一个文本元素序列

使用不同的编码方案编码为字节时,由与另一个文本元素序列相同的字节序列表示。也就是说,磁盘上的相同字节序列可能表示两个不同的文本元素序列,具体取决于编码方案!换句话说,为了弄清楚字节序列的含义,我们必须知道它是什么样的数据,因此要使用什么解码方案。

所以这是可能发生的事情。在 vi 中你写道:

s += "â"; // 131
s += "ä"; // 132

您的印象是 vi 将使用扩展 ASCII 表示这些字符,因此使用字节 131 和 132。但这是不正确的。vi 没有使用扩展的 ASCII,而是使用不同的方案 (UTF-8) 来表示这些字符,该方案恰好使用两个字节来表示这些字符中的每一个。

后来,当您在不同的编辑器中打开源代码时,该编辑器错误地认为该文件是扩展的 ASCII 并将其显示为扩展 ASCII。由于扩展 ASCII 对每个字符使用一个字节,因此它采用两个字节 vi 来表示每个字符,并为每个字节显示一个字符。

最重要的是,源代码使用扩展 ASCII 是不正确的,因此您假设这些字符将由值为 131 和 132 的单个字节表示是不正确的。

  1. 当我们使用基于范围的循环遍历 s 时,如何确定对于普通字符它必须增加 1 倍,对于扩展字符必须增加 2 倍!?

您的程序未执行此操作。在您的 ideone.com 示例中,字符打印正常,因为独立打印出表示这些字符的两个字节可以显示该字符。这里有一个例子可以清楚地说明这一点:现场示例。

std::cout << "Printed together: '";
std::cout << (char)0xC3;
std::cout << (char)0xA2;
std::cout << "'n";
std::cout << "Printed separated: '";
std::cout << (char)0xC3;
std::cout << '/';
std::cout << (char)0xA2;
std::cout << "'n";

Printed together: 'â'
Printed separated: '�/�'
'

' 字符是在遇到无效编码时显示的字符。

如果你问如何编写一个这样做的程序,答案是使用理解正在使用的编码细节的代码。要么获取一个理解 UTF-8 的库,要么自己阅读 UTF-8 规范。

您还应该记住,此处使用 UTF-8 仅仅是因为此编辑器和编译器默认使用 UTF-8。如果要使用不同的编辑器编写相同的代码并使用不同的编译器进行编译,则编码可能完全不同;假设代码是 UTF-8 可能与您之前假设代码是扩展 ASCII 一样错误。

您的终端可能使用 UTF-8 编码。 它使用 1 个字节作为 ASCII 字符,使用 2-4 个字节用于其他所有字符。

C++源代码的基本源字符集不包括扩展的 ASCII 字符(参见 ISO/IEC 14882:2011 中的 §2.3):

基本源字符集由 96 个字符组成:空格字符、表示水平制表符、垂直制表符、表单换行符和换行符的控制字符,以及以下 91 个图形字符:

a b c d e f g h i j k l m n o p q r s t u v w x y z

A B C D E F G H I J K L M N O P Q R S T U V W X Y Z

0 1 2 3 4 5 6 7 8 9

_ { } [ ] # ( ) <> % : ; . ? * + -/^ & | ∼ ! = , \ " '

因此,实现必须将这些字符从源文件映射到基本源字符集中的字符,然后再将它们传递给编译器。它们可能会映射到通用字符名称,遵循 ISO/IEC 10646 (UCS):

通用字符名称结构提供了一种命名其他字符的方法。

由通用字符名称 \UNNNNNNNN 指定的字符是 ISO/IEC 10646 中字符短名称为 NNNNNNNNN 的字符

;由通用字符名称 \uNNNN 指定的字符是 ISO/IEC 10646 中字符短名称为 0000NNNNN 的字符。

窄字符串文本中的通用字符名称(如您的情况)可以使用多字节编码映射到多个字符(参见 ISO/IEC 14882:2011 中的 §2.14.5):

在窄字符串文本中,由于多字节编码,通用字符名称可能映射到多个 char 元素。

这就是你看到的最后 3 个角色。

最新更新