编码问题

在 Windows 上编写包含中文字符串的程序几乎一定会遇上乱码问题,导致这个问题的因素有很多,本文将介绍目前我已知的一些原因以及比较好的解决办法。。

由于本人水平有限,只能从一些表面的方法来尽可能解决这些问题,但个人感觉以下内容并没有碰到真正的本质问题。

以下内容仅供参考,若有错误,敬请各位指正!

乱码出现的原因

乱码出现的原因?这个问题实际上答案很多人都知道,简单来说就是文件中保存字符用的字符集为了展示字符而用来解释字符编码所用的字符集不匹配。上面这句话比较绕,也可以直接看下图:

01

所以,实际上乱码出现的原因很简单,解决办法也很简单,我们只需要让两个不匹配的字符集匹配起来即可。

但实际解决过程并没有这么顺利,为了尽可能模拟实际开发,我们假设源文件统一使用 UTF-8 字符集,所以我们没办法通过修改源文件字符集来解决乱码问题了。在此基础上的一个解决乱码问题的方法是在用户运行程序前先修改终端的字符集,但该方法仅仅是自己骗自己罢了,因为你没办法让每个用户在运行程序前都自行修改终端字符集,当然,你可以在程序中添加一句修改终端字符集的系统调用,但这并不是对所有类型的程序都有效的通用方法。

为了真正解决这个问题,我们首先得了解一个程序从源文件到可执行程序,它们所保存的字符或字符串编码或许并不属于同一个字符集,我们得了解中间影响编码的具体环节有哪些。

编译过程影响编码的环节

我们最后需要展示出来的字符串的来源通常有两处:源文件中的字符串字面值和用户输入的字符串。其中,用户输入的字符串并不会受到编译过程的影响,因为这类字符串直到程序运行时我们才能获取,这些字符串输入时根据终端字符集将文字编码输入至程序,程序对这串编码进行一系列处理,这一系列过程都是已知的,因为程序是我们自己写的,我们清楚程序对输入的字符串做了什么,例如获取了字符串的长度、裁剪了字符串、转换了字符串的编码等等。

所以我们需要考虑的是字符串字面值的编码在编译过程中发生了什么。

  • 源文件字符集

    这是首个影响编码的环节。源文件字符集是什么,那么该文件中字符串字面值的编码就是按照该字符集对字符串编码得到的内容

  • C++ 字符及字符串前缀

    C++ 中字符及字符串的前缀会在编译过程对字符串字面值的编码产生影响,具体行为是从编译器认为的源文件字符集(下一点中的【选项1】)转换为字符或字符串前缀对应的字符集。具体的前缀和对应字符集详情见另一篇文章:字符类型与编码

  • 编译器采用的字符集

    编译器有两个选项,一个是选择待编译源文件的字符集,一个是选择可执行程序的执行环境采用的字符集,后续称前者为【选项1】、后者为【选项2】。当这两个选项相同时,编译过程不会发生额外的编码转换;但当二者不同时,会发生从【选项1】字符集到【选项2】字符集的转换,C++ 字符串前缀导致转换后的字符串不进行此次转换。

    默认情况下,GCC 编译器的【选项1】默认可以识别 UTF-8 和 UTF-8 with BOM 字符集,无法识别的字符集默认当做 UTF-8 字符集,而其【选项2】则默认为 UTF-8 字符集;而 MSVC 默认情况下的【选项1】可以识别 UTF-8/16/32 with BOM、当前用户代码页,无法识别的字符集默认当做当前用户代码页,其【选项2】默认为当前用户代码页

结论

总的来说,一个程序从源文件到可执行程序首先会被 C++ 字符及字符串前缀影响,进行从编译器【选项1】字符集到字符及字符串前缀对应字符集的转换,这一部分已经转换完成的字符及字符串将不会再参与后续编译器编码转换过程。而未经过转换的剩下的字符及字符串字面值将进行编译器【选项1】字符集到【选项2】字符集的转换,最后得到可执行程序。

避免乱码

选择源文件字符集

以开源社区为例,源文件通常采用 UTF-8 字符集,一般来说,我们也选择保持该规范。那么对于 GCC 编译器,我们并不需要指定【选项1】;而对于 MSVC 编译器,我们需要显式指定【选项1】为 UTF-8 字符集。

另一个折中的方法是源文件全部采用 UTF-8 with BOM 字符集,这样我们不需要为任何一个编译器显式选择【选项1】。但这样的方法可能并不适合一些老旧的编译器以及某些环境,主要是在 Windows 系统下支持良好。

指定编译器字符集选项

我们需要保证的是编译器【选项1】和源文件字符集匹配或能够自动识别,【选项2】和当前用户代码页匹配。

关于【选项1】的部分我们已经搞定了,所以接下来是关于【选项2】的部分。

在一些小型测试代码中,我们简单地在编译命令或构建脚本中指定【选项2】为当前用户代码页即可。通常我们只需要在 Windows 环境下使用 GCC(MinGW)编译器时指定【选项2】即可,因为首先 MSVC 的【选项2】默认就是当前用户代码页,其次而在非 Windows 环境下时,我们使用的编译器一般是 GCC,且此时用户环境通常为 UTF-8,是 GCC 【选项2】的默认值。

中文处理方法

对于程序中实际存在的中文,我们可以使用一种确定 Unicode 字符集在程序中来进行处理。简单来说,就是在字符串输入和输出的两个地方进行字符串编码转换。

例如,在输入端,我们得到的字符串编码通常是当前用户代码页字符集的编码,此时我们可以将输入得到的字符串转换为程序统一处理的 Unicode 编码,例如 UTF-16。对于字符串字面值,我们可以直接通过前缀来使字面值保存为需要统一处理的 Unicode 编码。

而在输出端,我们需要所有待输出的字符串编码都与当前用户代码页相匹配,即进行从程序统一处理字符集到当前用户代码页字符集的转换。

通过这样的方式,我们可以保证程序内部处理的字符串编码是统一的。需要注意的是一些 Unicode 字符集并非定长字符集,例如 UTF-16,大部分字符在该字符集下是 2 个字节,但部分字符会超过 2 字节,所以有些字符或字符串处理函数需要进行一定的修改,例如返回字符串长度的函数。