前言

本文将探索在编程过程中出现乱码问题的具体原因。之所以加了个前缀 “编程过程中” 是因为我并非是要说明乱码本身出现的原因即编码与解码阶段使用的编码不一致,这个原因很好理解并且也已经有很多人讲过了。

本文所探讨的是在编程过程中由于各种中间环节的影响,即使你清楚乱码产生的原因,也仍然可能无法避免地产生乱码的问题。

另外,由于在 Linux 环境下几乎不会遇到这样的问题(如果我们统一使用utf-8的文件格式与utf-8的用户字符集并且使用 GNU GCC 作为编译器的话),所以本文的全部内容都将基于 Windows 环境下的乱码问题进行介绍,中文环境将以gbk为准。

系列文章

一、三大影响因素

1. 源文件编码和终端字符集

相信很多人遇到这个问题之后进行搜索所得到的答案基本上都是源文件的编码(1)和终端字符集(2)不匹配,这两个虽然也是乱码的两个重要影响因素,但实际上这两个因素理论上来说是不应该去改变的,即我们应该保持源文件编码和终端字符集的差异。之所以这样说,主要有以下两个方面:

  1. 对于源文件编码,应该尽可能使用多字节编码,这样能在对源文件内容完整编码的同时最大程度上节约空间。而作为世界通用的utf-8和中国专用的gbk这两种编码来说,则通常选择utf-8编码,这也是开源社区最常用的编码。
  2. 对于终端字符集,网上会有大量的修改本地字符集的教程,但这种办法都是治标不治本的,即使是在程序内部内置修改本地字符集的代码,我想那也不是用户想要程序去做的事情。我们要做的应该是如何在源文件编码和终端字符集都保持其差异性的情况下使程序正常地输入输出。

为了完成这一目标,如果只了解以上所说的两个乱码影响因素是无论如何都解决不了问题的,我们还缺少一个最重要的第三因素将整个过程串联起来,从而才能找到隐藏在其中的乱码的决定性原因。

关于源文件的编码

对于一个一般的 C/C++ 源文件,其内容应该分为两部分,一是组成程序的语法部分,二是数据部分。其中数据部分比较特殊的就是字符串字面值。

在源文件中,其字符串字面值通常按照与源文件相同的编码进行保存,即一个utf-8编码的文件中的字符串字面值也将保存为utf-8编码。

请仔细理解这一点,将成为后续理解的基础。

2. 编译器的作用

很显然,连接在源文件和终端之间的一个东西就是编译器(3),这就是我们要找的第三大因素,下面将详细讲解编译器是如何影响编码的。

如果你使用 MSVC 工具集进行编译,那么你是否发现了这个现象:当源文件使用utf-8(带 BOM)编码时,终端字符集仍然保持GBK编码,此时使用 MSVC 对程序进行编译,得到的程序仍然能够进行正常的输入输出。但相应地如果使用 GCC 编译相同编码的文件,其输出则会产生乱码,这也侧面证明了编译器是在某种程度上影响着乱码问题的产生的。

实际上编译器基本上都会有两类选项,一个是设定输入源文件的编码,另一个是设定输出字符的编码,其中输出字符的编码通常还分为宽字符和窄字符两个细化的设定,本文只讨论窄字符的乱码问题,对于宽字符的编码问题希望读者能够理解原理,举一反三。

在后文中将编译器的这两类选项简称为设定1(输入源文件编码设定项)和设定2(输出窄字符编码设定项)。

编译器的这两个设定影响编码的具体过程如下所述:

  1. 首先编译器将按照设定1的值对源文件编码进行解释,所以如果设定1的值和源文件编码不相同,则通常会导致乱码。

    如果设定1的值和源文件编码都是多字节编码,比如一个是gbk,另一个是utf-8,那么程序通常可以正常编译,因为多字节编码的前128个字符通常是相同的,而组成程序语法部分所用到的字符全部都在这 128 个字符中。但相对的,对于数据部分的字符串则由于不同的解释,将会导致乱码。

    如果设定1的值是多字节编码,而源文件是双字节或四字节编码,则程序将无法通过编译,因为双字节或四字节编码与多字节编码的前128个字符编码上有差异。

  2. 然后编译器会根据设定2的值将字符串数据的编码从源文件编码转换为设定2的编码(实际上中间可能还会进行一次转换,即将源文件编码转换为编译器能够处理的编码,但这一步对整个乱码问题没有影响,所以此处忽略了),所以源文件中的字符串字面值编码首先通过设定1正确识别,然后根据设定2设定的字符集的值转换为对应的字符串字面值编码,最后保存在可执行文件中进行输出。

二、两个决定性原因

根据上一节所述的内容可以很清楚地总结出发生乱码问题的两个原因:

  1. 编译器设定1的值与源文件编码不一致
  2. 最终输出的字符串编码所使用的字符集与终端字符集不匹配

三、编译器之间的区别

本文将关注 MSVC 和 GCC 两个最常用的编译器及它们之间的区别。

1. MSVC

首先是 MSVC,它可以自动识别utf-16(无论是大端还是小端,以及无论是否有 BOM)和utf-8(BOM)的文件,其余的编码都将默认采用用户字符集(在本文中限定为gbk)进行解释,包括utf-8(不带 BOM)也将采用改字符集进行解释。

总结如下:

  1. 设定1(/source-charset:[charset]):自动跟随utf-16utf-8(BOM)文件的编码值,否则默认使用用户字符集(在本文中限定为gbk
  2. 设定2(/execution-charset:[charset]):默认为用户字符集(在本文中限定为gbk
  3. 设定3(输出宽字符编码设定项):默认为Unicode

2. GCC

相比于 MSVC,GCC 就显得并不那么灵活了,总结如下:

  1. 设定1(-finput-charset=[charset]):默认为utf-8(无论是否带 BOM)
  2. 设定2(-fexec-charset=[charset]):默认为utf-8
  3. 设定3(输出宽字符编码设定项:-fwide-exec-charset=[charset]):默认为utf-16utf-32,具体根据wchar_t类型的宽度决定,而wchar_t类型的宽度又通常依赖于平台实现,通常 Windows 实现为 2 字节宽,而 Linux 实现为 4 字节宽。