為什么會(huì)出現(xiàn)亂碼
首先痰洒,我們需要有的概念是亂碼的問題是由編碼和解碼方式引起的。涉及到編碼方式的地方有3個(gè):
- 源碼字符集
- 執(zhí)行字符集
- 運(yùn)行環(huán)境字符集
源碼字符集確切的說是編譯器認(rèn)為源碼文件的編碼方式媒佣,執(zhí)行字符集是可執(zhí)行程序采用的編碼方式,而運(yùn)行環(huán)境字符集則是環(huán)境支持的編碼方式。編譯程序處理字符串的過程畏铆,實(shí)際上是首先讀入字符的二進(jìn)制數(shù),根據(jù)編碼格式到另一種編碼格式轉(zhuǎn)換策略得到另外一串二進(jìn)制數(shù)吉殃,所以1->2可能有二進(jìn)制數(shù)的變化,而3則是通過既定的編碼方式來解讀2中的二進(jìn)制數(shù)為字符(這里為什么說可能呢辞居,因?yàn)?和2如果是相同的編碼是不需要變化的)。
那么具體是哪些地方引起錯(cuò)誤呢蛋勺?在解答之前先介紹理解該問題的先驗(yàn)知識(shí)(由于我的運(yùn)行環(huán)境是window簡(jiǎn)體中文版瓦灶,所以以下的locale編碼就是指GBK編碼):
- msvc2013編譯程序時(shí),處理源碼字符集時(shí)抱完,有BOM標(biāo)識(shí)符的則正確識(shí)別(實(shí)際上目前就是有無BOM的utf-8)贼陶,無BOM則使用本地Locale字符集(隨系統(tǒng)設(shè)置而變),執(zhí)行字符集默認(rèn)用本地Locale字符集(其他msvc版本在看完本文甚至可以根據(jù)自己實(shí)驗(yàn)猜測(cè)處理)巧娱。
- gcc編譯程序時(shí)碉怔,默認(rèn)兩者都是uft-8,有finput-charset源碼字符集和fexec-charset執(zhí)行字符集則按照設(shè)置禁添。
那么亂碼的原因有:
①編譯器解讀源碼字符集錯(cuò)誤撮胧。如我是utf-8的源碼,因?yàn)椴粠om你當(dāng)成locale老翘,執(zhí)行字符集也是locale所以不需要轉(zhuǎn)換芹啥,而本來utf-8到locale是需要轉(zhuǎn)換的锻离。
②源碼字符集到執(zhí)行字符集的轉(zhuǎn)換錯(cuò)誤。如本來把識(shí)別正確的源碼字符集locale轉(zhuǎn)成執(zhí)行字符集中的utf-8墓怀,結(jié)果你給我指定了錯(cuò)誤了轉(zhuǎn)換方式汽纠,說讓我通過xxx編碼轉(zhuǎn)utf-8的策略轉(zhuǎn)(Note:這是錯(cuò)誤的表述,看到下面你就明白傀履,實(shí)際上這里的錯(cuò)誤只能是應(yīng)為轉(zhuǎn)換算法的錯(cuò)誤)疏虫。
③字符解析錯(cuò)誤。如果現(xiàn)在程序中的字符串二進(jìn)制是utf-8的啤呼,結(jié)果你非要說執(zhí)行字符集是loacle卧秘,那么解析肯定會(huì)出錯(cuò)。
還需要理解的包括下面的知識(shí):
- windows console控制臺(tái)代碼頁為locale官扣,即把程序中的字符串二進(jìn)制表示當(dāng)locale執(zhí)行字符集來解讀
- 字符串二進(jìn)制的表示形式不需要編譯翅敌,直接拷貝到執(zhí)行程序的二進(jìn)制中
亂碼情況解析
接下來內(nèi)容的實(shí)例基于csdn作者“在水一方”博文中舉的“我是中文”的例子(文末有引用),他的博文在我理解這個(gè)問題的本質(zhì)過程中幫助很大惕蹄。這里就套用他的例子的蚯涮,一方面我比較懶,不想舉其他例子卖陵,另一方面通過驗(yàn)證他的例子遭顶,也佐證了我自己的想法。
直接上例子(這里說的都是源碼字符集):
const char * str = "我是漢字"
//用GBK編碼等價(jià)于
const char * str = "\xce\xd2\xca\xc7\xba\xba\xd7\xd6";
//用utf-8編碼等價(jià)于
const char * str = "\xe6\x88\x91\xe6\x98\xaf\xe6\xb1\x89\xe5\xad\x97";
//note:這里的等價(jià)于就是說當(dāng)計(jì)算機(jī)到內(nèi)存中來處理的時(shí)候泪蔫,中文讀入的就是等價(jià)于的二進(jìn)制數(shù)
翻譯一下就是棒旗,“我是漢字”這幾個(gè)字,在GBK編碼下就是保存的“\xce\xd2\xca\xc7\xba\xba\xd7\xd6”這樣一串二進(jìn)制撩荣,而utf-8則是保存的“\xe6\x88\x91\xe6\x98\xaf\xe6\xb1\x89\xe5\xad\x97”铣揉。這里可以使用Notepad++進(jìn)行驗(yàn)證。
字符解析錯(cuò)誤亂碼
- 編譯環(huán)境:vs2013(msvc2013編譯器)餐曹,源碼文件字符集GBK
- 運(yùn)行環(huán)境:Windows簡(jiǎn)體中文下的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;
運(yùn)行程序得到下圖結(jié)果:
根據(jù)結(jié)果我們可以看到2是亂碼的逛拱,而漢字表現(xiàn)出了和GBK下二進(jìn)制數(shù)據(jù)一樣的結(jié)果。有了前面的先驗(yàn)知識(shí)按照前面先驗(yàn)的亂碼原因①②③來理解:
①對(duì)于不帶bom源碼的文件台猴,msvc2013當(dāng)成locale處理朽合,而源碼字符集恰巧是locale,讀入源碼字符集沒問題饱狂。這里需要“我是漢字”字符串變?yōu)槎M(jìn)制數(shù)曹步,并記錄源碼字符集。
②源碼字符集和執(zhí)行字符集都是locale嗡官,不需要轉(zhuǎn)換箭窜,沒轉(zhuǎn)換自然轉(zhuǎn)換沒問題。到此衍腥,字符串的二進(jìn)制表示的直接拷貝到了執(zhí)行程序中磺樱。
③2把執(zhí)行程序中“\xe6\x88\x91\xe6\x98\xaf\xe6\xb1\x89\xe5\xad\x97”——“我是中文”uft-8編碼下的二進(jìn)制,當(dāng)成了GBK編碼來解析婆咸,所以出現(xiàn)了類型③亂碼竹捉。
Note:請(qǐng)用notepad++檢驗(yàn),以便理解尚骄。
在上面程序的基礎(chǔ)上块差,我們添加測(cè)試函數(shù)的函數(shù)體前添加一段預(yù)定義,這是c++11對(duì)執(zhí)行字符集的支持:
//讓編譯器編譯生成程序的執(zhí)行字符集為utf-8
#if _MSC_VER >= 1600
#pragma execution_character_set("utf-8")
#endif
再次運(yùn)行程序倔丈,得到如下的結(jié)果:
首先看到12和上面結(jié)果一樣憨闰,有人這里就有疑問了。你說的字符串的二進(jìn)制表示直接拷貝我也理解需五,但是現(xiàn)在我的執(zhí)行字符集是utf-8啊鹉动,那我解讀第一個(gè)和第二個(gè)的結(jié)果不應(yīng)該是這個(gè)啊。那你可能忘掉了我之前的一個(gè)先驗(yàn)知識(shí)了宏邮,console不認(rèn)識(shí)utf-8泽示,它仍然會(huì)把這串二進(jìn)制當(dāng)成locale來解讀,所以這里和上面的表現(xiàn)結(jié)果是一樣的蜜氨。
下面來看3是怎么回事械筛,①②流程下來:
①源碼為locale,編譯器也默認(rèn)認(rèn)為源碼字符集是locale(編譯器這是瞎貓碰到死耗子飒炎,蒙對(duì)了埋哟!),解讀正確郎汪。
②編譯器正確知道源碼字符集的情況下定欧,需要轉(zhuǎn)化成指定的字符集,自然是會(huì)給出正確的轉(zhuǎn)化策略怒竿。
最終砍鸠,編譯通過編碼轉(zhuǎn)換策略做了一次從 “\xce\xd2\xca\xc7\xba\xba\xd7\xd6”到“\xe6\x88\x91\xe6\x98\xaf\xe6\xb1\x89\xe5\xad\x97”的轉(zhuǎn)換,所以程序中又是“我是中文”uft-8編碼下的二進(jìn)制了耕驰,最終又回到了2的情況——類型③亂碼爷辱。
轉(zhuǎn)換錯(cuò)誤亂碼(反證)
- 編譯環(huán)境:QtCreator(MinGW gcc編譯器),源碼文件字符集utf-8
- 運(yùn)行環(huán)境:Windows簡(jiǎn)體中文下圖形界面
下面看一段代碼:
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編碼朦肘,下面代碼等價(jià)于:
//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();
運(yùn)行程序得到下圖結(jié)果:
這里我不給出詳細(xì)的分析了饭弓,通過第1個(gè)標(biāo)簽和第2個(gè)標(biāo)簽結(jié)果都正常,可以驗(yàn)證出gcc編譯的默認(rèn)規(guī)則——默認(rèn)源碼字符集和執(zhí)行字符集都是uft-8媒抠,且知道了Qt中QString::fromxxx()函數(shù)的作用了弟断。而標(biāo)簽2和標(biāo)簽3的對(duì)比可以知道,當(dāng)環(huán)節(jié)②出錯(cuò)趴生,就出現(xiàn)亂碼了阀趴。過程是編譯器把讀入的utf-8編碼下的二進(jìn)制當(dāng)成了loacle來解析昏翰,這時(shí)就解析成了所謂的那串“亂碼”,然后正確轉(zhuǎn)換成了uft-8編碼下的該“亂碼”(Note:這里兩次亂碼實(shí)際的二進(jìn)制是不一樣的哦刘急,只是編碼形式不同才有的相同結(jié)果棚菊,你明白我的意思嘛?)叔汁。有人又要疑惑了统求,不對(duì)啊,你明明說這是個(gè)類型②的錯(cuò)誤据块,怎么我看著像是類型①亂碼呢码邻。其實(shí)如果你能這么疑惑,說明你是真的懂了另假,這里確實(shí)是一個(gè)類型①的亂碼像屋。實(shí)際上這里的源碼字符集到執(zhí)行字符集的算法是api內(nèi)部實(shí)現(xiàn)的,所以我們面對(duì)這種情況的時(shí)候②都不會(huì)出問題的浪谴。當(dāng)然了开睡,像你這種亂碼都沒有理解的人來說,去實(shí)現(xiàn)這個(gè)算法苟耻,那我是不敢用篇恒,說不定就會(huì)產(chǎn)生類型②亂碼了,哈哈凶杖。
總結(jié)
由于Qt的出現(xiàn)就是為了跨平臺(tái),所以QString中統(tǒng)一采用utf-16存儲(chǔ)字符串胁艰。所有源碼中的字符串存放到QString中時(shí),都需要經(jīng)過一次到utf-16的正確轉(zhuǎn)換智蝠。在qt5之前腾么,有兩種解決方式解決亂碼:
QString::fromxxx();
QTextCodec::setCodecForxxx();
相信大家看了前面已經(jīng)明白這兩個(gè)函數(shù)是意思,這里要提醒一句的就是杈湾,兩種方式最終在QString中存放的解虱,都是字符串在unicode編碼形式下的二進(jìn)制。
寫在最后
這系列的文章將會(huì)以自己學(xué)習(xí)后理解的知識(shí)點(diǎn)分享為主漆撞,希望吾之所得亦可為汝所得殴泰。在2017年3月18日重新更新文章時(shí),我刪掉了與知識(shí)點(diǎn)無關(guān)的表述浮驳。只是希望讓正努力從“不求甚解”到“先去理解清楚一些以釋重負(fù)”轉(zhuǎn)變的你悍汛,不會(huì)因?yàn)槠^長(zhǎng)望而卻步。如有疑問至会,歡迎提問离咐,如有高見,煩請(qǐng)指點(diǎn)奉件。
參考:
- qt中文亂碼問題
- QString亂談(2)
- 從此亂碼是路人
- 以及n多的內(nèi)容宵蛀。上面三篇博文尤其前兩篇帶我真正理解了qt中中文亂碼的原因