C 中的 ASCII、ISO 8859-1、Unicode 是如何工作的?

ASCII, ISO 8859-1, Unicode in C how does it work?

嗯,我真的很怀疑,C 是如何使用编码的,首先我有一个 C 文件,用 ISO 8859-1 编码保存,内容 test.c,当 运行 字符 ÿ 在 linux 控制台上显示不正确的程序,我知道默认情况下它使用 utf-8,但如果 utf-8 使用与 ISO 8859-1 相同的 256 个字符,为什么不程序正确显示“ÿ”字符?另一个问题,为什么 test2 正确显示'ÿ'字符? test2.c 文件是 UTF-8 而 file.txt 是 UTF-8?换句话说,编译器不是在抱怨宽度是多字符吗?

test1.c

  // ISO 8859-1
  #include <stdio.h>

  int main(void)
  {
    unsigned char c = 'ÿ';
    putchar(c);
    return 0;
  }

  $ gcc -o test1 test1.c
  $ ./test1
  $ ▒

test2.c

  // ASCII
  #include <stdio.h>

  int main(void) 
  {

     FILE *fp = fopen("file.txt", "r+");
     int c;

     while((c = fgetc(fp)) != EOF)
        putchar(c);
     return 0;
 }

file.txt: UTF-8 abcdefÿghi

  $ gcc -o test2 test2.c
  $ ./test2
  $ abcdefÿghi

嗯,就是这样,如果你能帮我提供详细信息,我将不胜感激,:)

这里的问题是 unsigned char 表示大小为 8 位(从 0 到 255)的无符号整数。 C 使用 ASCII 值来表示字符。一个 ASCII 字符只是一个从 0 到 127 的整数。例如,A 是 65。

当您使用 'A' 时,编译器理解 65。但是,'ÿ' 不是 ASCII 字符,它是扩展的 ASCII 字符(值为 152)。从技术上讲,它可以放在 unsigned char 中,但 C 标准要求语法 '' 包含标准 ASCII 字符。

所以这就是第一个示例不起作用的原因。

现在是第二个。非 ASCII 字符不能放入单个字符中。处理有限 ASCII 集之外的字符的方法是使用多个字符。当您将 ÿ 写入文件时,您实际上是在写入该字符的二进制表示。如果您使用 UTF-8 表示,这意味着在您的文件中有两个 8 位数字 0xC30xBF.

当您在 test2.c 的 while 循环中读取文件时,在某个时候,c 将取值 0xC3,然后在下一次迭代中取 0xBF .这两个值将被赋予putc。然后,当显示时,这两个值一起将被解释为 ÿ.

putc 最终写入字符时,它们最终会被您的终端应用程序读取。如果支持UTF-8编码,则可以理解0xC3后跟[=​​21=]的意思,显示一个ÿ.

因此,在第一个示例中,您没有看到 ÿ 的原因是代码中 c 的值实际上(可能)是 0xC3不代表任何字符。

更具体的例子:

#include <stdio.h>

int main()
{
    char y[3] = { 0xC3, 0xBF, '[=10=]' };
    printf("%s\n", y);
}

这将显示 ÿ,但如您所见,需要 2 个字符才能显示。

由于多种原因,字符编码可能会造成混淆。以下是一些解释:

在 ISO 8859-1 编码中,字符 y 带有分音符 ÿ(原本是 的连字ij) 被编码为字节值 0xFF (255)。 Unicode 中的前 256 个代码点 do 对应于与 ISO 8859-1 相同的字符,但是流行的 Unicode UTF-8 编码使用 2 个字节来表示大于 127 的代码点, 所以 ÿ 在 UTF-8 中编码为 0xC3 0xBF.

当您读取文件 file.txt 时,您的程序一次读取一个字节并将其原封不动地输出到控制台(遗留系统上的行结尾除外) , ÿ 被读取为2个独立的字节依次输出, 终端显示 ÿ 因为终端选择的语言环境也使用UTF-8编码。

更让人困惑的是,如果源文件使用UTF-8编码,"ÿ"是长度为2的字符串,'ÿ'被解析为多字节字符常量。多字节字符常量非常混乱且不可移植(值可以是 0xC3BF 或 0xBFC3,具体取决于系统),强烈建议不要使用它们,编译器应配置为在看到一个时发出警告 (gcc -Wall -Wextra)。

更令人困惑的是:在许多系统上默认签名的类型 char。在这种情况下,字符常量 'ÿ'(ISO 8859-1 中的单个字节)的值为 -1,类型为 int,无论您如何在源代码中编写它: '7''\xff' 也将具有 -1 的值。这样做的原因是与 "ÿ"[0] 的值一致,char 与值 -1 一致。这也是宏EOF.

最常用的值

在所有系统上,getchar() 和类似的函数如 getc()fgetc() return 值在 0UCHAR_MAX 之间或特殊EOF 的负值,因此编码为 ISO 8859-1 的字符 ÿ 的文件中的字节 0xFF 被 return 编辑为值 0xFF255,如果 char 是有符号的,它与 'ÿ' 比较不同,如果源代码是 UTF-8,它也与 'ÿ' 不同。

根据经验,不要在字符常量中使用 non-ASCII 个字符,不要假设用于字符串和文件内容的字符编码,并配置编译器使 char 无符号默认情况下 (-funsigned-char).

如果您处理外语,强烈建议对所有文本内容(包括源代码)使用 UTF-8。请注意,使用此编码,non-ASCII 个字符被编码为多个字节。学习UTF-8 encoding,非常简单优雅,使用库来处理大写等文本转换。

如果 utf-8 使用与 ISO 8859-1 相同的 256 个字符。不,这里有混乱。在 ISO-8859-1(又名 Latin1)中,256 个字符确实具有相应 Unicode 字符的代码点值。但是 utf-8 对 0x7f 以上的所有字符都有特殊的编码,所有代码点在 0x80 和 0xff 之间的字符都表示为 2 个字节。例如字符 é U+00e9 在 ISO-8859-1 中表示为单字节 0xe9,但在 utf-8 中表示为 2 个字节 0xc3 0xa9.

有关 wikipedia page 的更多参考。

在 MacOS 上用 clang 很难重现:

$ gcc -o test1 test1.c
test1.c:6:23: warning: illegal character encoding in character literal [-Winvalid-source-encoding]
    unsigned char c = '<FF>';
                      ^
1 warning generated.

$ ./test1
?

$ gcc -finput-charset=iso-8859-1 -o test1 test1.c
clang: error: invalid value 'iso-8859-1' in '-finput-charset=iso-8859-1'

MacOS 上的 clang 默认使用 UTF-8。

以 UTF-8 编码:

$ gcc -o test1 test1.c
test1.c:6:23: error: character too large for enclosing character literal type
    unsigned char c = 'ÿ';
                      ^
1 error generated.

调试所有警告和错误,我们得到一个具有正确字符串文字和字节数组的解决方案:

// UTF-8
  #include <stdio.h>

// needed for correct strings
  #include <string.h>

  int main(void)
  {
    char c[] = "ÿ";
    int len  = strlen(c);
    printf("len: %u c[0]: %u \n", len, (unsigned char)c[0] );

    putchar(c[0]);
    return 0;
  }

$ ./test1
len: 2 c[0]: 195
?

十进制195是十六进制C3,正好是字符ÿ:

的UTF-8字节序列的第一个字节
$ uni identify ÿ
     cpoint  dec    utf-8       html       name
'ÿ'  U+00FF  255    c3 bf       &yuml;     LATIN SMALL LETTER Y WITH DIAERESIS (Lowercase_Letter)
                    ^^ <-- HERE

现在我们知道我们必须输出2个字节和代码:

    char c[] = "ÿ";
    int len  = strlen(c);

    for (int i=0; i < len; i++) {
        putchar(c[i]);
    }
    printf("\n");

$ ./test1 
ÿ

程序test2.c只是读取字节并输出它们。如果输入是 UTF-8,那么输出也是 UTF-8。这只是保留编码。

要将 Latin-1 转换为 UTF-8,我们需要以特殊方式对其进行打包。对于两个字节的 UTF-8,我们需要一个开始字节 110x xxxx(开始的位数是序列的字节长度)和一个连续字节 10xx xxxx.

我们现在可以编码了:

  #include <stdio.h>
  #include <string.h>
  #include <stdint.h>

  int main(void)
  {
    uint8_t latin1 = 255; // code point of 'ÿ'  U+00FF  255

    uint8_t byte1 = 0b11000000 | ((latin1 & 0b11000000) >> 6);
    uint8_t byte2 = 0b10000000 |  (latin1 & 0b00111111);

    putchar(byte1);
    putchar(byte2);

    printf("\n");

    return 0;
  }

$ ./test1
ÿ

这仅适用于 ISO-8859-1(“真正的”Latin-1)。许多名为“Latin-1”的文件编码为 Windows/Microsoft CP1252。