为什么会出现乱码
首先,我们需要有的概念是乱码的问题是由编码和解码方式引起的。涉及到编码方式的地方有3个:
源码字符集
执行字符集
运行环境字符集
源码字符集(the source character set):源码文件时使用何种编码格式保存的。
执行字符集(the execution character):可执行程序内保存的是何种编码(程序执行时内存中字符串编码)
gcc 运行字符集设置参数
-finput-charset=charset //设置源码字符集为charset
-fexec-charset=charset //设置执行字符集为charset
-fwide-exec-charset=charset //设置宽字符串的执行字符集为charset
msvc 运行字符集设置参数
-execution-charset:utf-8 // 指明程序执行时使用UTF-8字符集
-source-charset:utf-8 // 指明源码文件的编码为UTF-8字符集
源码字符集确切的说是编译器认为源码文件的编码方式,执行字符集是可执行程序采用的编码方式,而运行环境字符集则是环境支持的编码方式。编译程序处理字符串的过程,实际上是首先读入字符的二进制数,根据编码格式到另一种编码格式转换策略得到另外一串二进制数,所以1->2可能有二进制数的变化,而3则是通过既定的编码方式来解读2中的二进制数为字符(这里为什么说可能呢,因为1和2如果是相同的编码是不需要变化的)。
那么具体是哪些地方引起错误呢?在解答之前先介绍理解该问题的先验知识(由于我的运行环境是window简体中文版,所以以下的locale编码就是指GBK编码):
msvc2013编译程序时,处理源码字符集时,有BOM标识符的则正确识别(实际上目前就是有无BOM的utf-8),无BOM则使用本地Locale字符集(随系统设置而变),执行字符集默认用本地Locale字符集(其他msvc版本在看完本文甚至可以根据自己实验猜测处理)。
gcc编译程序时,默认两者都是uft-8,有finput-charset源码字符集和fexec-charset执行字符集则按照设置。
那么乱码的原因有:
①编译器解读源码字符集错误。如我是utf-8的源码,因为不带bom你当成locale,执行字符集也是locale所以不需要转换,而本来utf-8到locale是需要转换的。
②源码字符集到执行字符集的转换错误。如本来把识别正确的源码字符集locale转成执行字符集中的utf-8,结果你给我指定了错误了转换方式,说让我通过xxx编码转utf-8的策略转(Note:这是错误的表述,看到下面你就明白,实际上这里的错误只能是应为转换算法的错误)。
③字符解析错误。如果现在程序中的字符串二进制是utf-8的,结果你非要说执行字符集是loacle,那么解析肯定会出错。
还需要理解的包括下面的知识:
windows console控制台代码页为locale,即把程序中的字符串二进制表示当locale执行字符集来解读
字符串二进制的表示形式不需要编译,直接拷贝到执行程序的二进制中
乱码情况解析
接下来内容的实例基于csdn作者“在水一方”博文中举的“我是中文”的例子(文末有引用),他的博文在我理解这个问题的本质过程中帮助很大。这里就套用他的例子的,一方面我比较懒,不想举其他例子,另一方面通过验证他的例子,也佐证了我自己的想法。
直接上例子(这里说的都是源码字符集):
翻译一下就是,“我是汉字”这几个字,在GBK编码下就是保存的“\xce\xd2\xca\xc7\xba\xba\xd7\xd6”这样一串二进制,而utf-8则是保存的“\xe6\x88\x91\xe6\x98\xaf\xe6\xb1\x89\xe5\xad\x97”。这里可以使用Notepad++进行验证。
字符解析错误乱码
编译环境:vs2013(msvc2013编译器),源码文件字符集GBK
运行环境:Windows简体中文下的Console命令行
下面看一段代码:
运行程序得到下图结果:
根据结果我们可以看到2是乱码的,而汉字表现出了和GBK下二进制数据一样的结果。有了前面的先验知识按照前面先验的乱码原因①②③来理解:
①对于不带bom源码的文件,msvc2013当成locale处理,而源码字符集恰巧是locale,读入源码字符集没问题。这里需要“我是汉字”字符串变为二进制数,并记录源码字符集。
②源码字符集和执行字符集都是locale,不需要转换,没转换自然转换没问题。到此,字符串的二进制表示的直接拷贝到了执行程序中。
③2把执行程序中“\xe6\x88\x91\xe6\x98\xaf\xe6\xb1\x89\xe5\xad\x97”——“我是中文”uft-8编码下的二进制,当成了GBK编码来解析,所以出现了类型③乱码。
Note:请用notepad++检验,以便理解。
在上面程序的基础上,我们添加测试函数的函数体前添加一段预定义,这是c++11对执行字符集的支持:
再次运行程序,得到如下的结果:
首先看到12和上面结果一样,有人这里就有疑问了。你说的字符串的二进制表示直接拷贝我也理解,但是现在我的执行字符集是utf-8啊,那我解读第一个和第二个的结果不应该是这个啊。那你可能忘掉了我之前的一个先验知识了,console不认识utf-8,它仍然会把这串二进制当成locale来解读,所以这里和上面的表现结果是一样的。
下面来看3是怎么回事,①②流程下来:
①源码为locale,编译器也默认认为源码字符集是locale(编译器这是瞎猫碰到死耗子,蒙对了!),解读正确。
②编译器正确知道源码字符集的情况下,需要转化成指定的字符集,自然是会给出正确的转化策略。
最终,编译通过编码转换策略做了一次从 “\xce\xd2\xca\xc7\xba\xba\xd7\xd6”到“\xe6\x88\x91\xe6\x98\xaf\xe6\xb1\x89\xe5\xad\x97”的转换,所以程序中又是“我是中文”uft-8编码下的二进制了,最终又回到了2的情况——类型③乱码。
转换错误乱码(反证)
编译环境:QtCreator(MinGW gcc编译器),源码文件字符集utf-8
运行环境:Windows简体中文下图形界面
下面看一段代码:
运行程序得到下图结果:
这里我不给出详细的分析了,通过第1个标签和第2个标签结果都正常,可以验证出gcc编译的默认规则——默认源码字符集和执行字符集都是uft-8,且知道了Qt中QString::fromxxx()函数的作用了。而标签2和标签3的对比可以知道,当环节②出错,就出现乱码了。过程是编译器把读入的utf-8编码下的二进制当成了loacle来解析,这时就解析成了所谓的那串“乱码”,然后正确转换成了uft-8编码下的该“乱码”(Note:这里两次乱码实际的二进制是不一样的哦,只是编码形式不同才有的相同结果,你明白我的意思嘛?)。有人又要疑惑了,不对啊,你明明说这是个类型②的错误,怎么我看着像是类型①乱码呢。其实如果你能这么疑惑,说明你是真的懂了,这里确实是一个类型①的乱码。实际上这里的源码字符集到执行字符集的算法是api内部实现的,所以我们面对这种情况的时候②都不会出问题的。当然了,像你这种乱码都没有理解的人来说,去实现这个算法,那我是不敢用,说不定就会产生类型②乱码了,哈哈。
总结
由于Qt的出现就是为了跨平台,所以QString中统一采用utf-16存储字符串。所有源码中的字符串存放到QString中时,都需要经过一次到utf-16的正确转换。在qt5之前,有两种解决方式解决乱码:
相信大家看了前面已经明白这两个函数是意思,这里要提醒一句的就是,两种方式最终在QString中存放的,都是字符串在unicode编码形式下的二进制。
写在最后
这系列的文章将会以自己学习后理解的知识点分享为主,希望吾之所得亦可为汝所得。在2017年3月18日重新更新文章时,我删掉了与知识点无关的表述。只是希望让正努力从“不求甚解”到“先去理解清楚一些以释重负”转变的你,不会因为篇幅过长望而却步。如有疑问,欢迎提问,如有高见,烦请指点。
参考:
以及n多的内容。上面三篇博文尤其前两篇带我真正理解了qt中中文乱码的原因