背景介绍

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

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

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

这两种方法都可以让你的程序看上去没有问题,但实际上细想一下就知道这是治标不治本的方法。首先,最不推荐的就是第一种,要么修改代码编码,在中文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即可),库会自己将编码转换为对应平台和环境下可识别的编码。

背景介绍

对于刚开始学习 C/C++ 的初学者来说,乱码问题可能困扰过大部分人。

目前网上大部分解决方案都是修改用户自己的环境,但这在实际软件分发场景下几乎是不可行的,因为你不应该要求自己软件的用户先进行环境的修改,你自己安装的其他软件也没有让你这样做的。

现在也可以直接针对这个问题去询问 AI,或许会给你一些不同的答案。当然,如果你愿意,也可以看看我写的这篇文章,希望能解决你的问题。

本文将从影响编码显示的三个方面对乱码原因进行拆解,并在之后给出或许最合适的解决方案。

影响编码的三因素

源文件编码

源文件编码直接决定了参与编译的字符串字面值的编码,后文我们将其称为【输入编码】。

目前几乎所有开源社区最通用的编码都采用 UTF-8,而不是只在部分地区适用的 GBK、GB2312 等编码。

网络上部分解决乱码问题的方案是直接让你将源文件修改为 GBK 或 GB2312 编码,这确实能一定程度上解决问题。如果你的代码只是小团队内进行开发,也不上传到其他开源社区,且都只用中文版 Windows,那么直接这么做也是可行的。而且也不会有之前说的分发前让用户自行修改环境的问题。

但如果你的情况与上述情况相反,那么就得学会在源文件采用 UTF-8 编码时也不会乱码的解决方法。

后文将假定我们的【输入编码】都是 UTF-8 编码。

用户环境编码(终端编码)

另一个会导致乱码的因素就是用户环境编码,也就是你的终端显示所用的编码,后文我们将其称为【输出编码】。

在中文版 Windows 下,【输出编码】通常是 GB2312。在其他地区的 Windows 下,【输出编码】则是对应地区的字符集,但通常不会是 UTF-8.

Linux 和 Mac 系统下,【输出编码】通常是 UTF-8,这也是在 Linux 和 Mac 系统下初学者一般不会遇到乱码问题的原因

编译器编码转换

上述两个因素是大多数人都熟知的因素,所以只是简单介绍了一下。但编译器的编码转换规则却很少有初学者了解,且在你搜索乱码问题解决方案时,也几乎不会有教程提到这一方面的内容。而这却恰好是问题的关键。

前面我们说了有【输入编码】和【输出编码】,而编译器也有两个选项,分别对应了这两个编码,后文我们将其称为【输入选项】和【输出选项】(注意,这两组编码并没有自动关联,实际的【输入编码】和【输出编码】取决于文件和环境,而【输入选项】和【输出选项】则通常默认保持固定值不变)

而这些选项对实际乱码问题的影响取决于下面这个原则:

原则一:当编译器的【输入选项】和【输出选项】不一致时,编译时会将字符串从【输入选项】的编码自动转换为【输出选项】的编码

这是乱码的关键,也是我们合理解决乱码问题的关键。

具体的乱码原因与解决方案参见下一节。此处我再针对不同编译器的默认行为进行一个补充:

常用的编译器一般是 MSVC(安装 VS 时自带的) 和 GCC,其他的编译器的默认行为大多和 GCC 一致,此处不再介绍。

  • GCC:其【输入选项】和【输出选项】默认都设置为 UTF-8,故默认不会发生编码转换
  • MSVC:【输入选项】可以自动识别用户环境编码(中文版 Windows 也就是 GBK/GB2312)、UTF-8 with BOM(注意不是 UTF-8,具体区别可以自行搜索),当源文件并非其可自动识别的编码时,会将其自动当作用户环境编码的文件。【输出选项】默认为用户环境编码。

乱码原因分析

表象上,当【输入编码】和【输出编码】不一致时,就会导致乱码。但这并不是根本原因,在遇到其他乱码现象时,由于忽略了第三个影响因素,所以不可避免的会导致分析出错。

分析一

在我们固定【输入编码】为 UTF-8,【输出编码】为 GB2312 时,在默认情况下,无论是 GCC 还是 MSVC,它们都会乱码且应该是同一种乱码,解析如下:

  • 对于 GCC 编译器,其默认不会发生编码转换,故原本的 UTF-8 编码的字符直接显示在 GB2312 终端上,导致乱码
  • 对于 MSVC 编译器,由于我们使用的是 UTF-8 而不是 UTF-8 with BOM,MSVC 无法自动识别这个编码,所以将其当成了 GB2312 编码(用户环境编码)的源文件进行处理,而其默认的【输出编码】也是用户环境编码,即 GB2312,所以也不会发生自动编码转换。所以还是原本的 UTF-8 编码直接显示在 GB2312 终端上,导致乱码

分析二

那么,为什么我们修改了【输入编码】或【输出编码】之后就能够正常了呢,解析如下:

  • 当我们修改【输入编码】为 GB2312 之后,和之前一样两个编译器都还是默认不发生自动转换,所以现在是 GB2312 编码的字符显示在 GB2312 终端上,正确。但这里隐藏了一个问题,对于 GCC 来说,它把源文件始终当成 UTF-8 进行读取,但是它现在实际上是 GB2312,如果源文件里全是英文还好说,但如果里面有中文,是有概率会读取错误导致编译失败的。例如:

    1
    printf("你好,世界");

    上述中文字符串并未严格设置,此处只讲原理。

    "你好,世界"这几个中文字符编码中的如果有一个字节和\"的编码一样(因为中文字符都是多字节的,所以这种情况完全有可能出现),那么,字符串将被提前结束。而被误识别的引号到反括号之间还剩了一些其他的中文字符的编码以及一个真正的引号,所以将导致编译失败

  • 当我们修改【输出编码】为 UTF-8 之后,和之前一样的原因两个编译器不发生自动转换,所以 UTF-8 编码的字符显示在 UTF-8 编码的终端上,正确。但和之前一样,这里也隐藏了一个问题,如果是修改了整个系统的编码,倒还好,MSVC 或许会表现正常(未验证)。但如果仅仅是在终端临时修改了终端编码,那么或许并不会对 MSVC 的默认识别行为进行修改,它可能还是将源文件默认识别为 GB2312 的编码(未验证),从而导致和上面一样的原因。

这里我们遇到了一个问题,也就是【输入编码】和编译器的【输入选项】不匹配的情况。在这种情况下,如果是【输入选项】和【输出选项】一致,由于不发生编码转换,所以效果还是将【输入编码】显示在了【输出编码】的终端上,二者不匹配时还是乱码,而且跟修改之前相比还多了编译失败的风险。而如果【输入选项】和【输出选项】不一致,那么在第一层错误读取之后还经过了一次转换,错上加错几乎一定会乱码

所以这里我们引入第二个原则:

原则二:【输入选项】应该和【输入编码】保持一致,【输出选项】应该和【输出编码】保持一致

解决方案

结语