Qt开发中文显示乱码


为什么会出现乱码

首先,我们需要有的概念是 乱码的问题是由编码和解码方式引起的 。涉及到编码方式的地方有3个:

  1. 源码字符集
  2. 执行字符集
  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作者“在水一方”博文中举的“我是中文”的例子(文末有引用),他的博文在我理解这个问题的本质过程中帮助很大。这里就套用他的例子的,一方面我比较懒,不想举其他例子,另一方面通过验证他的例子,也佐证了我自己的想法。

直接上例子(这里说的都是源码字符集):



const char * str = "我是汉字"
//用GBK编码等价于
const char * str = "\xce\xd2\xca\xc7\xba\xba\xd7\xd6";
//用utf-8编码等价于
const char * str = "\xe6\x88\x91\xe6\x98\xaf\xe6\xb1\x89\xe5\xad\x97";
//note:这里的等价于就是说当计算机到内存中来处理的时候,中文读入的就是等价于的二进制数


翻译一下就是,“我是汉字”这几个字,在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命令行

下面看一段代码:



char * cc = "\xce\xd2\xca\xc7\xba\xba\xd7\xd6";
std::cout << cc << std::endl;
char * cc1 = "\xe6\x88\x91\xe6\x98\xaf\xe6\xb1\x89\xe5\xad\x97";
std::cout << cc1 << std::endl;
char * cc2 = "我是汉字";
std::cout << cc2 << std::endl;


运行程序得到下图结果:




Qt开发中文显示乱码_字符串


根据结果我们可以看到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对执行字符集的支持:



//让编译器编译生成程序的执行字符集为utf-8
#if _MSC_VER >= 1600
#pragma execution_character_set("utf-8")
#endif


再次运行程序,得到如下的结果:




Qt开发中文显示乱码_编译器_02


首先看到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简体中文下图形界面

下面看一段代码:



MainWindow w;
QLabel *lb1 = new QLabel(&w);
QLabel *lb2 = new QLabel(&w);
QLabel *lb3 = new QLabel(&w);
lb1->setText(QString("我是汉字"));
lb1->resize(100, 20);
lb1->move(120, 120);
lb2->setText(QString::fromUtf8("我是汉字"));
lb2->resize(100, 20);
lb2->move(120,160);
//由于源码是utf-8编码,下面代码等价于:
//lb3->setText(QString::fromLocal8Bit("我是汉字"));
lb3->setText(QString::fromLocal8Bit(
"\xe6\x88\x91\xe6\x98\xaf\xe6\xb1\x89\xe5\xad\x97"));
lb3->resize(100, 20);
lb3->move(120,200);
w.show();


运行程序得到下图结果:




Qt开发中文显示乱码_编译器_03


这里我不给出详细的分析了,通过第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::fromxxx();
QTextCodec::setCodecForxxx();


相信大家看了前面已经明白这两个函数是意思,这里要提醒一句的就是, 两种方式最终在QString中存放的,都是字符串在unicode编码形式下的二进制。

写在最后

这系列的文章将会以自己学习后理解的知识点分享为主,希望吾之所得亦可为汝所得。在2017年3月18日重新更新文章时,我删掉了与知识点无关的表述。只是希望让正努力从“不求甚解”到“先去理解清楚一些以释重负”转变的你,不会因为篇幅过长望而却步。如有疑问,欢迎提问,如有高见,烦请指点。

-----------------------



msvc:QMAKE_CXXFLAGS += -execution-charset:utf-8

msvc:QMAKE_CXXFLAGS += -source-charset:utf-8

msvc:QMAKE_CXXFLAGS_WARN_ON += -wd4819

gcc:QMAKE_CXXFLAGS += -finput-charset=charset:utf-8

gcc:QMAKE_CXXFLAGS += -fexec-charset=charset:utf-8

gcc:QMAKE_CXXFLAGS += -fwide-exec-charset=:utf-8


-----------------------




QTextCodec::setCodecForCStrings(QTextCodec::codecForName("UTF-8"));  
QTextCodec::setCodecForLocale(QTextCodec::codecForName("UTF-8"));
QTextCodec::setCodecForTr(QTextCodec::codecForName("UTF-8"));


网上很多人一碰到编码问题就无脑的Copy上面3行……

从Qt5开始只剩下setCodecForLocale这一个了,只是影响Qt对toLocal8Bit相关函数的编码方式





{  
// Qt默认会使用本机编码,所以对于中文系统,下面这句设置是多余的
QTextCodec::setCodecForLocale(QTextCodec::codecForName("GBK"));

QString str1("你好Hello");
QByteArray bLocal = str1.toLocal8Bit(); // 受setCodecForLocale影响,会转换为设定的编码。如果本机不支持指定编码,则会按toLatin1处理
QByteArray baLatin1 = str1.toLatin1(); // 不受setCodecForLocale影响,强制转换为ISO-8859-1编码
QByteArray bUtf8 = str1.toUtf8(); // 不受setCodecForLocale影响,强制转换为UTF-8编码
qDebug() << str1; // 正常,Qt会将UTF-16转换为UTF-8输出
qDebug() << baLatin1; // 乱码,用UTF-8编码输出Latin1字节流
qDebug() << bLocal; // 乱码,用UTF-8编码输出GBK字节流
qDebug() << bUtf8; // 正常,用UTF-8编码输出UTF-8字节流

QString str2 = QString::fromLocal8Bit(bLocal);
qDebug() << str2; // 正常,因为上面显式指定字节流来自本机编码,而bLocal正是本机编码GBK

str2 = QString::fromLatin1(bLocal);
qDebug() << str2; // 乱码,bLocal是GBK编码,但却指定了以Latin1方式去读取,肯定会乱码

// 字节流来自UTF-8
str2 = QString::fromUtf8("\xE4\xBD\xA0\xE5\xA5\xBD\x48\x65\x6C\x6C\x6F");
qDebug() << str2; // 正常

// Qt默认采用UTF-8处理字符串,所以不用显式地去调用fromUtf8
str2 = QString("\xE4\xBD\xA0\xE5\xA5\xBD\x48\x65\x6C\x6C\x6F");
qDebug() << str2; // 正常

}




在Qt中,QString会用UTF-16编码存储,而qDebug()等I/O函数会以UTF-8编码处理。

其实转换后的字节流是正确的,只是显示时用了和字节流不同的编码方式处理导致乱码

所以当要在Qt中输入输出非UTF-8字符串时应该先转换一下

在源码中要写入非英文字符的话建议使用转义的方式,也就是上面“\xE4\xBD...”这种,这也是官方推荐的方式

为了方便将字符串转换为UTF8转义字符,写了一个小工具

2014-5-6补充:

从vs2010sp1和vs2013开始就已经支持UTF-8的源码文件了,只用在工程里加入一句"#pragma execution_character_set("UTF-8")"即可。不用再做上面的转义了。



============= End