你是否遇到过C语言或者Java程序中的汉字字符串输出为乱码的现象,也许你去查询过解决办法,得到的结论就是简单地把源码文件转换为GBK编码存储,然而并未深思其中的原因。本文会解释其原因以及正确的应对方式(不转换源码的编码)。首先明确本文的操作环境:Windows操作系统(中文)、C语言编译器为MinGW-GCC。下面给出两个示例程序:
// a.c(utf-8)
#include <stdio.h>
int main(void)
{
printf("%s", "你好");
return 0;
}
// a.java(utf-8)
public class a {
public static void main(String[] args) {
System.out.println("你好");
}
}
以上源码文件均使用UTF-8编码,然后我们打开PowerShell,编译两个文件:gcc a.c、javac a.java,然后运行:.\a.exe、java a,会发现输出的均不是“你好”,而是乱码的“浣犲ソ”。实际上,以UTF-8编码的“你好”和以GBK编码的“浣犲ソ”的二进制序列是一样的(E4 BD A0 E5 A5 BD),你发现其中端倪了吗?
造成这种现象的原因在于,首先说C语言,默认情况下,GCC编译器认为编译前的源码文件以及编译后的二进制文件中的字符编码都是UTF-8,而在Windows(中文)中系统会认为二进制文件中的字符编码是GBK。
所以默认情况下,使用GCC编译,“你好”的UTF-8编码就保持着原本的二进制形式(E4 BD A0 E5 A5 BD)存储在了二进制文件中,而在运行时Windows(中文)则以为这是一段使用GBK编码后的字符串,所以就解码为了“浣犲ソ”。
对于GCC编译器,可以使用选项-finput-charset来指定源码文件中的字符编码,-fexec-charset用来指定编译之后的二进制文件中的字符编码。
所以我们使用gcc -finput-charset=utf-8 -fexec-charset=gbk a.c来进行编译(可以不指定,因为GCC默认认为源码是UTF-8),此时源码中UTF-8编码的“你好”会被编译器转换为GBK编码的“你好”存储到二进制文件中,再运行就会正常显示“你好”了。-finput-charset=utf-8
再说Java,源码编译后的字符串总是以modified UTF-8的形式存储在class文件中,并且在加载进JVM运行时会被转换为UTF-16编码存储在内存中,并在实际打印出来时被转换为平台默认字符编码(或者在打印函数中手动传入字符编码参数来指定),换句话说,从编译之后到运行的过程中,Java都能够自动地正确处理字符编码,所以乱码的问题实际出现在编译过程中。
在Windows(中文)中,系统默认字符编码是GBK,那么javac就会默认假定输入的Java源码文件是以GBK编码的。但实际上我们是以UTF-8存储的源码,所以UTF-8编码的“你好”被javac编译器认为是GBK编码的“浣犲ソ”(还记着二者的二进制序列一样吗?),然后就被转换为modified UTF-8编码的“浣犲ソ”存储进了class文件,最后就会打印出“浣犲ソ”。
对于javac编译器,可以使用选项-encoding来指定源码文件中的字符编码。
所以我们使用javac -encoding utf-8 a.java来进行编译,源码文件中UTF-8编码的”你好“就会被正确的转换为modified UTF-8编码的”你好“存入class文件中了,最后就能正确的打印出”你好“。
总结
对于GCC编译器,可以使用选项-finput-charset来指定源码文件中的字符编码,-fexec-charset用来指定编译之后的二进制文件中的字符编码。
默认情况下,GCC编译器认为编译前的源码文件以及编译后的二进制文件中的字符编码都是UTF-8,而在Windows(中文)中系统会认为二进制文件中的字符编码是GBK。
对于javac编译器,可以使用选项-encoding来指定源码文件中的字符编码。
在Windows(中文)中,系统默认字符编码是GBK,那么javac就会默认假定输入的Java源码文件是以GBK编码的。
参考资料:
- C语言:https://blog.csdn.net/Blazar/article/details/79253475
- Java:https://www.zhihu.com/question/30977092/answer/50266867
- 《Java核心技术 卷2:高级特性(原书第11版)》7.6.6 源文件的字符编码