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 位数字 0xC3
和 0xBF
.
当您在 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 带有分音符 ÿ(原本是 的连字i 和 j) 被编码为字节值 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 值在 0
和 UCHAR_MAX
之间或特殊EOF
的负值,因此编码为 ISO 8859-1 的字符 ÿ
的文件中的字节 0xFF 被 return 编辑为值 0xFF
或 255
,如果 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 ÿ 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。
嗯,我真的很怀疑,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 位数字 0xC3
和 0xBF
.
当您在 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 带有分音符 ÿ(原本是 的连字i 和 j) 被编码为字节值 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 值在 0
和 UCHAR_MAX
之间或特殊EOF
的负值,因此编码为 ISO 8859-1 的字符 ÿ
的文件中的字节 0xFF 被 return 编辑为值 0xFF
或 255
,如果 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
,正好是字符ÿ
:
$ uni identify ÿ
cpoint dec utf-8 html name
'ÿ' U+00FF 255 c3 bf ÿ 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。