第一步,在Windows 10环境下,使用Notepad++编写如下源代码,并保存到文件main.c当中。
#include
int main(void)
{
puts("China");
puts("中国");
return 0;
}
这段代码的意思,是在控制台输出两个字符串,一个是China,另一个是中国。
第二步,使用如下gcc(gcc 8.2.0,下同)命令编译上述源代码,生成可执行文件main.exe。
gcc main.c -o main.exe
第三步,运行刚刚生成的main.exe,输出结果如下。
China
中国
上述程序代码和输出结果如预期一般,一切正常。下面我们再来看一个程序。
第一步,在Windows 10环境下,使用Notepad++编写如下源代码,并保存到文件main.utf8.c当中。
#include
int main(void)
{
puts("China");
puts("中国");
return 0;
}
这段代码跟上面的程序代码完全相同。但是,跟上一个程序不同的是,这次保存文件main.utf8.c时选择的源文件字符集编码格式是UTF-8。
第二步,使用如下gcc命令编译上述源代码,生成可执行文件main.utf8.exe。
gcc main.utf8.c -o main.utf8.exe
第三步,运行刚刚生成的main.utf8.exe,输出结果如下。
China
涓浗
可以看到,这次的输出与上一个程序的输出不同。字符串China如预期一样正常输出了,但是中国这两个字并没有被输出,取而代之的是输出了涓浗这几个奇怪的字符。
同样的源代码,怎么源文件的字符编码不同,输出结果就不同了呢?
既然是源文件编码不同导致了输出结果不同,那就首先来看一下两个源文件的内容。
我自己写了一个以二进制方式读取文本文件的函数,代码如下。这段代码的意思就是将指定文件以二进制方式读入内存,然后,逐个字节输出它的十六进制表示以及对应的字符。当然,如果你喜欢用其他的工具,那也很好。
#define BUFFER_LEN 1024
void readFileWithBinary(const char *filePath)
{
FILE *pFile = fopen(filePath, "rb");
if (NULL == pFile)
{
printf("Failed to open file:%s\n", filePath);
return;
}
unsigned char buffer[BUFFER_LEN];
int readCount;
int i;
while (!feof(pFile))
{
readCount = fread(buffer, sizeof(unsigned char), BUFFER_LEN, pFile);
for (i = 0; i < readCount; i++)
{
unsigned char c = buffer[i];
printf("%X(%c) ", c, c);
}
}
fclose(pFile);
}
需要说明的是,在简体中文环境下,Windows 10默认的字符集编码是GBK,对应的代码页是936。也就是说第一个源文件main.c的编码格式是GBK。
用这个二进制读取程序读取上面的源代码文件main.c,可以看到其中的部分输出如下。
) 9( ) 70(p) 75(u) 74(t) 73(s) 28(() 22(") 43(C) 68(h) 69(i) 6E(n) 61(a) 22(") 29()) 3B(;) D(
) A(
) 9( ) 70(p) 75(u) 74(t) 73(s) 28(() 22(") D6(? D0(? B9(? FA(? 22(") 29()) 3B(;) D(
) A(
可以看到,在main.c文件中,China这个字符串的5个字母及其对应的编码的十六进制表示如下表。
字符 | C | h | i | n | a |
编码 | 43 | 68 | 69 | 6E | 61 |
而中国这两个字的存储对应的是以下4个连续字节:
D6 | D0 | B9 | FA |
如果我们查阅GBK编码表,可以确认,D6D0对应的是中字,B9FA对应的是国字。
然后用上面同样的二进制读取程序读取另一个源文件main.utf8.c,可以看到其中的部分输出如下。
) 9( ) 70(p) 75(u) 74(t) 73(s) 28(() 22(") 43(C) 68(h) 69(i) 6E(n) 61(a) 22(") 29()) 3B(;) D(
) A(
) 9( ) 70(p) 75(u) 74(t) 73(s) 28(() 22(") E4(? B8(? AD(? E5(? 9B(? BD(? 22(") 29()) 3B(;) D(
) A(
可以看到,在以UTF-8编码保存的main.utf8.c中,China这个字符串的5个字母及其对应的编码的十六进制表示如下表。可以发现它与main.c当中的China的编码完全一样。
字符 | C | h | i | n | a |
编码 | 43 | 68 | 69 | 6E | 61 |
而在main.utf8.c文件当中,中国这两个字的存储对应的是以下6个连续字节:
E4 | B8 | AD | E5 | 9B | BD |
如果我们查阅UTF-8编码表,可以确认,前三个字节的组合编码E4B8AD对应的是中字,后三个字节的组合编码E59BBD对应的是国字。
对比可以发现,中国这两个字在GBK编码的main.c文件当中和UTF-8编码的main.utf8.c文件当中的编码是不同的。
了解了源文件存储的编码的不同,我们还需要了解不同字符编码的源文件对于GCC编译器生成的可执行文件有什么影响。如果不了解这一点,那就不能从整体上了解源文件字符编码是如何影响可执行文件的输出结果的。
现在,仍然使用上面的那个二进制读取程序来读取GBK编码的main.c源文件对应的可执行文件main.exe,其中有一部分输出如下。
...43(C) 68(h) 69(i) 6E(n) 61(a) 0( ) D6(? D0(? B9(? FA(? 0( ) 0( ) ...
对比源文件main.c的内容,可以看到main.exe文件里面,China这5个字符的存储是使用了连续的5个字节,而且跟main.c文件当中的5个连续字节完全一样。中国这2个汉字的存储是使用了连续的4个字节(D6 D0 D9 FA),而且跟main.c文件当中的4个连续字节完全一样。
同样可以读取UTF-8编码的main.utf8.c源文件对应的可执行文件main.utf8.exe,其中有一部分输出如下。
...43(C) 68(h) 69(i) 6E(n) 61(a) 0( ) E4(? B8(? AD(? E5(? 9B(? BD(? 0( ) 0( ) 0( ) 0( )...
对比源文件main.utf8.c的内容,可以看到main.utf8.exe文件里面,China这5个字符的存储是使用了连续的5个字节,而且跟main.utf8.c文件当中的5个连续字节完全一样。中国这2个汉字的存储是使用了连续的6个字节(E4 B8 AD E5 9B BD),而且跟main.utf8.c文件当中的6个连续字节完全一样。
我们可以发现,无论源文件的编码是什么,通过前面的GCC编译过程产生的可执行文件(main.exe和main.utf8.exe)当中,存储的字符的编码和生成他们的源文件(main.c和main.utf8.c)当中存储的字符的编码并没有区别。换句话说,GCC在编译过程中直接拷贝了源文件当中的那一串字节。当执行puts等控制台输出的时候,程序(main.exe和main.utf8.exe)直接把那一串连续字节输出到标准输出stdout。至于怎么把这一串编码转换为相应的输出字符,那就跟操作系统和控制台程序有关系了。
再来看一下那个“不正常”的输出。
China
涓浗
它不正常的地方是原本应该希望它输出中国这两个字,现在却输出了涓浗这三个字符。查一下GBK编码表就会发现,这三个字符及其对应的GBK编码十六进制表示如下表。
字符 | 涓 | | 浗 |
GBK编码 | E4B8 | ADE5 | 9BBD |
如果把它们连在一起,刚好就是中国这两个字的UTF-8编码的值(连续6个字节:E4 B8 AD E5 9B BD)。所以那个“不正常”的真相就是:控制台把main.utf8.exe输出的这6个字节当作是GBK字符流来解释了。那么控制台为什么会按照GBK来处理那6个字节呢?如果我们看一下控制台的属性,会发现它的当前代码页这一项的内容是:936(ANSI/OEM-简体中文GBK)。这也是Windows 10简体中文版系统的ANSI字符集。
现在我们可以看一下改变控制台窗口的代码页是否能让main.utf8.exe输出预期的结果。在控制台窗口运行以下命令将该窗口的代码页修改为65001,也就是UTF-8字符集。
chcp 65001
现在再来运行main.utf8.exe,可以得到如下输出。
China
中国
搞定!!!
不过,如果我们现在运行main.exe的话,会得到如下“不正常”的输出。
China
й
可以看到,й这个奇怪的字符替代了原来936代码页情况下输出的中国两个字。而й这个字符的UTF-8编码恰好是D0B9,也就是中国这两个字的GBK(代码页936)编码。
这也再次说明了控制台代码页(字符集)对程序的字符输出的影响。
以上内容主要提出了C语言程序源文件编码格式不同对字符输出的影响,通过研究源文件编码、可执行文件编码和控制台程序的编码设置,对于影响字符输出的这几个相关因素有了初步的了解。然而,以上分析还不算究竟,所涉及的场景还不够全面,对于其他的影响因素还没有涵盖。我将在接下来的几篇文章中陆续进行分析。
以上内容,难免有错误之处。请大家不吝赐教!