背景介绍

对于大部分初学者以及一些老手来说,程序乱码这个问题或许都并没有深入探究过其最优的解决方案。

网上充斥着大量的教程,基本上都是翻来覆去的教你怎么给自己的电脑改个环境,不外乎这两种:

  • 修改代码编码或终端编码,终端编码也有临时修改和永久修改的方法。这是程序之外的手动修改的部分
  • 也可以通过程序内的一些方法来解决,例如通过系统调用在程序执行时修改终端编码,或通过本地化库去完成类似的工作

这两种方法都可以让你的程序看上去没有问题,但实际上细想一下就知道这是治标不治本的方法。首先,最不推荐的就是第一种,要么修改代码编码,在中文Windows环境下,终端编码通常是GB2312,所以将代码编码修改为GB2312就可以了,但就大趋势而言,源文件应该尽量使用UTF-8编码,在这种情况下,对于第一种方法,就只能临时或永久地修改终端编码环境,但问题就在这,先不说修改完编码环境之后你自己的电脑在使用上是否会出现各种奇怪的乱码错误,就算你自己写的程序在你的电脑上现在能够正常运行了,那么当你想要把程序分发给别人的时候呢,难道还得让别人在用之前先让他修改一下电脑环境吗?所以方法一是完全不行的。

相对来说,方法二的可行性就高很多了,但对于一些老旧的编译器或语言标准来说,这种方法可能并不生效,同时,即使生效也仅针对命令行应用,泛用性并不好。

本文所要介绍的方法是网络上遍布的教程中提到比较少的方法,即通过编译器选项去控制,首先,需要先理解编译器在编译期间对产生乱码的字符串字面值做了什么。

编译流程——编码转换相关

如果你除了最常用的 GCC 编译器之外,还用过微软的 MSVC(即安装 Visual Studio 任意版本附带的编译工具,注意不是 VSCode),并且用它编译过曾经产生乱码的代码,那么你或许会产生一个疑问,为什么使用 MSVC 就不会乱码呢。但如果你再多探索一下,你就会发现,首先使用 VS 创建的源文件默认都是 GBK 编码,如果你将其修改为 UTF-8,就会发现它仍然会乱码了,但它和 GCC 编译时的表现又不太一样,**MSVC 编译 UTF-8 编码的文件时,可能会编译失败,但 GCC 不会,这是为什么?**此处的问题暂时先按下不表。

如果再继续探索一下 MSVC 的行为,将源文件设置为 UTF-8 with BOM(BOM是一个文件编码标识,放在文件的最前端以供其他工具识别)的编码格式,就会发现得到的程序又不乱码了。到此,你或许会想:噢!将源文件改为加了 BOM 标识的 UTF-8 编码就好了吧,但实际上当你使用 GCC 对同样的代码进行编译时,乱码仍然存在。

上述问题的原因很显然是编译器在编译时的某种行为产生了区别,这种行为就是编译器会对源代码中的字符串字面值做编码转换,具体表述如下:

重要原理

编译器在编译时保存有两个编码值,一个是源文件的编码,一个是目标文件的编码,当二者不同时,就会将代码中的字符串字面值从源文件编码转换为目标文件编码。

对于 GCC 来说,其源文件编码值默认为 UTF-8,目标文件编码也是默认为 UTF-8,所以在默认情况下它在编译时不会对字符串字面值做任何转换,最终效果就是 UTF-8 编码的字符串显示在了 GB2312 编码的终端上导致乱码。

而 MSVC 相对于 GCC 来说,在编码识别方面就更加智能一些,它能够自动识别GBK和UTF-8 with BOM编码的源文件(实际上还有其他可自动识别的编码,可以自行搜索),然后生成的目标文件编码默认为 GBK,当源文件是任何可识别的编码时,就可以将字符串字面值从源文件编码转换为 GBK(当然,如果源文件是 GBK 编码则不发生转换),从而正确显示。但是,如果源文件是任何无法自动识别的编码时(例如 UTF-8 编码),就会将其当作 GBK 编码的文件进行读取。

根据这样的编译器行为,我们就可以回答之前的一些问题了:

  • 当我们使用 UTF-8 编码作为源文件编码时,二者具体乱码的原因是什么?且为什么 MSVC 可能会编译失败:
    • 对于 GCC,源文件被正确读取,但默认没有编码转换,所以最终 UTF-8 编码显示在 GB2312 的终端上
    • 对于 MSVC,此时 UTF-8 被当作 GBK 读取,对于组成代码逻辑的英文字符来说并不会导致编译失败,但是源文件中的中文字符串,由于编码的识别错误,可能会导致在整个文件内容的编码中,用于标识字符串结束的反引号被吞掉,即被迫和前面的编码组合在了一起,导致语法错误(此处的详细原因可能说明有误,但基本逻辑没错),从而导致编译失败。而如果编译成功,由于 UTF-8 被当作 GBK 读取了,目标文件编码也默认是 GBK,所以不发生编码转换,所以仍然是 UTF-8 显示在 GB2312 的终端上。
  • 为什么将源文件修改为 GBK 就不会乱码了:
    • 首先修改为 GBK 后,MSVC 能够正确识别,并且不会进行编码转换,对于 GCC 来说,虽然不能正确识别,但也不会进行编码转换,并且由于某种原因(此处读者如有疑惑,可以自行查询),将 GBK 识别为 UTF-8 并不像将 UTF-8 识别为 GBK 一样会大概率导致编译失败,所以看上去就像正常了一样。

如何控制——编译器选项

直接说结论吧:

  • 对于 GCC 有以下选项
    • -finput-charset=utf-8:控制源文件编码,编码值可自行修改
    • -fexec-charset=gb2312:控制目标文件编码,编码值可自行修改
  • 对于 MSVC 有以下选项
    • /source-charset:utf-8:控制源文件编码,编码值可自行修改
    • /executable-charset:gb2312:控制目标文件编码,编码值可自行修改
    • /utf-8:该选项将同时把上述两种编码设置为utf-8

我们设置的目标应该是让其能够正确识别源文件编码,并且转换为正确的目标文件编码。

结语

作为编程初学者,最好不要在乱码这个问题上耽误太多时间,不要过度研究这个问题,而应该关注语言和程序逻辑本身。

在实际开发中,一般会使用成熟的库作为应用框架进行开发,一般来说它们有自己的字符串类,而它们针对各种平台都做了适配处理,而用户通常只需要将上述的输入和输出编码都简单地设置为 UTF-8 即可(对于 GCC 不需要添加任何其他的选项,对于 MSVC 只需要添加/utf-8即可),库会自己将编码转换为对应平台和环境下可识别的编码。