徹底解決QT中文碼亂問題
讀完本文,讓你徹底明白Windows下中文亂碼的問題。一勞永逸地解決這個(gè)困擾很多同學(xué)的問題。
前言
在桌面開發(fā)過程中,由于Qt的跨平臺特性,以及更加先進(jìn)的庫封裝。比起MFC,用著不知道要爽多少。Qt獨(dú)創(chuàng)的信號槽機(jī)制,也大大方便了開發(fā)者。可以讓開發(fā)者把更多的精力放在業(yè)務(wù)的邏輯上,而不是語言和庫的各種細(xì)節(jié)上。
可是,在使用的過程中,不少朋友在中文Windows系統(tǒng)下,遇到了亂碼的問題。著實(shí)頭痛,網(wǎng)上搜了一圈,有時(shí)能解決問題,有時(shí)不知道什么原因的情況下又出現(xiàn)了奇怪的問題。同樣的問題在cocos2d-x中也會(huì)出現(xiàn)。
小伙伴們不要灰心,這個(gè)問題連大佬們都頭痛。哈哈~~,請看下面的案例。
上面的問題來自《Cocos2d-x實(shí)戰(zhàn):C++卷》,大佬也很無奈啊。
今天,讓我們來自己剖析一下這個(gè)問題。并最終找到一勞永逸的解決方案。
在開始前,我們先來羅列一下遇到的幾種情況:
- 完全正常。(人品大爆發(fā)啊)
- 直接亂碼。(哎,時(shí)運(yùn)不濟(jì))
- 編譯報(bào)錯(cuò)——C4819,C2001、C2143。(這是犯了什么天條了嗎?)
- 很小心的使用,可能正常。有時(shí)正常,有時(shí)編譯報(bào)錯(cuò),有時(shí)末尾的字是亂碼,前面的正常。(這是什么鬼?。?。
細(xì)心的小伙伴還總結(jié)出了,偶數(shù)個(gè)中文字符正常,奇數(shù)個(gè)就不行了。后面再加個(gè)英文字符,前面的顯示正常,后面一個(gè)字符亂碼。(我也太難了吧~~~)
一、字符編碼
要徹底理解這個(gè)問題,我們需要從字符編碼說起,小伙伴們稍微有點(diǎn)耐心,這個(gè)其實(shí)很容易理解。字符編碼說白了就是一張對照表。
1.1 ASCII編碼
這個(gè)編碼很容易,就用了一個(gè)字節(jié)進(jìn)行編碼,只能表示英文字符和標(biāo)點(diǎn)符號。這里我就不過多贅述了,百度一下,就有很多文章有詳細(xì)講解。
ASCII編碼表
1.2 中文編碼
計(jì)算機(jī)剛開始被發(fā)明的時(shí)候,只有ASCII編碼。也就是說只有英文,那我們怎么辦呢?沒有人幫我們做,那只有自己來了,在1980年,國家標(biāo)準(zhǔn)總局發(fā)布了GB2312,其實(shí)就是一張中文的編碼對照表。這也不是很復(fù)雜的東西,因?yàn)閱蝹€(gè)字節(jié)只有256種可能,也就是說,最多只能表示256種字符。那么我們就再多用一個(gè)字節(jié)唄,在GB2312中,中文就用2個(gè)字節(jié)進(jìn)行表示。2^16 = 65536,有這么多種可能,編碼漢字綽綽有余了。
當(dāng)然,考慮到兼容ASCII編碼,當(dāng)?shù)谝粋€(gè)字符數(shù)字小于127時(shí),就表示ASCII字符,用一個(gè)字節(jié)就夠了。當(dāng)遇到第一個(gè)字符大于127時(shí),就要結(jié)合第二個(gè)字符來決定是哪個(gè)中文字符了。
剛開始GB2312把6000多個(gè)中文編了進(jìn)去,后來發(fā)現(xiàn)不夠用,又增加了20000多個(gè)字符(包括繁體字),編碼方案名稱改為GBK。再后來,又增加了幾千個(gè)少數(shù)民族字符,編碼方案名稱改為GB18030。到這里,我們就知道GB2312、GBK、GB18030的編碼方式是一脈相承的。為了后面敘述的方便,我們統(tǒng)稱這種為GBK編碼。
1.3 Unicode編碼
在中國使用GBK編碼方案的同時(shí),其他國家和地區(qū)為了使用自己的文字,也紛紛進(jìn)行對自己的語言文字進(jìn)行編碼。造成的結(jié)果就是,不通用!不同語音的操作系統(tǒng)下編輯的文檔,在另一臺不同語音的計(jì)算機(jī)中打開就是亂碼。
隨著全球化的發(fā)展,急需一種統(tǒng)一的編碼方案,來解決這種混亂的局面。
最終,ISO拿出了Unicode編碼,廢棄所有地區(qū)性的編碼方案。重新編碼,所有的字符統(tǒng)一采用2個(gè)字節(jié)進(jìn)行存儲(chǔ)。
GBK編碼方案和Unicode編碼完全不同,這也是亂碼的根源。
二、文件編碼
2.1 UTF-8編碼
雖然上文中講到ISO將字符進(jìn)行了重新編碼,并發(fā)布了Unicode。每個(gè)字符采用2個(gè)字節(jié),16位進(jìn)行編碼。對于使用英語的國家來說,原來采用的是ASCII編碼,那么所有的文件大小都會(huì)變成原來的2倍。這個(gè)浪費(fèi)太大了,于是UTF-8就出現(xiàn)了。
如果用語言描述UTF-8,有些復(fù)雜。我們來舉個(gè)例子,就很容易明白了。
比如,“中”這個(gè)字,Unicode編碼為:0x4E2D。用二進(jìn)制寫就是(0100-1110-0010-1101),那么用UTF-8,怎么進(jìn)行表示呢?
1110 0100
1011 1000
1010 1101
我來解釋一下,第一個(gè)字節(jié),前面的4位中有連續(xù)的3個(gè)1,表示這個(gè)字符需要有3個(gè)字節(jié)組成。
第二個(gè)字節(jié),前面的2位10,表示上接前面的字節(jié),后面的6位是編碼。
第三個(gè)字節(jié)同第二個(gè)字節(jié),前面的10和后面的編碼。
也就是說,16位的Unicode編碼,被分散到3個(gè)字節(jié)中。
好麻煩啊……的確,遇到中文或其他多字節(jié)編碼的字符是有點(diǎn)麻煩,但是如果是英文字符,直接就用ASCII編碼保存了。直接完全兼容原來的英文文檔,他們就是有這么多的優(yōu)越性,沒辦法,畢竟計(jì)算機(jī)技術(shù)來自他們那兒。
2.2 ANSI編碼
這又是什么編碼?細(xì)心的小伙伴會(huì)發(fā)現(xiàn),你在Windows系統(tǒng)上用記事本編輯完文件,點(diǎn)另存為的時(shí)候,右下角默認(rèn)的編碼就是ANSI。這是Windows為了兼容各種不同的編碼,而這樣做的。
其實(shí),他的做法非常簡單,如果遇到小于127的編碼,就是ASCII編碼,計(jì)算機(jī)都認(rèn)識這個(gè)編碼,對于大于127的編碼,也不用管那么多了,按原樣保存就行了。
三、一勞永逸地消除亂碼
在解決問題前,我們再稍微了解一些背景知識,小伙伴們不要著急??!
3.1 UTF-8和UTF-8-BOM
UTF-8都夠復(fù)雜了,還來個(gè)UTF-8-BOM??
其實(shí),不必?fù)?dān)心,這個(gè)也是非常簡單的。
讓我們先來看個(gè)例子:
上圖,我們在記事本中寫入“中文”,然后,以utf-8保存。
再用notepad++查看存入的內(nèi)容,以十六進(jìn)制顯示。這樣沒有問題。
但是,如果再次打開,還會(huì)正確顯示嗎?記事本怎么知道我們是按utf-8存儲(chǔ)的呢?如果這個(gè)十六進(jìn)制的串,用GBK解碼就是“涓枃”,是不是有點(diǎn)眼熟???我們遇到亂碼的時(shí)候,也經(jīng)常是這種類似的字符。
現(xiàn)在,記事本工作得好好的,但是他有些時(shí)候會(huì)不會(huì)認(rèn)錯(cuò)呢?還真會(huì),用記事本新建一個(gè)文本文件,輸入“聯(lián)通”,保存,再打開。你是不是看到了微軟對聯(lián)通滿滿的惡意?哈哈O(∩_∩)O哈哈~
其實(shí),各種軟件在處理文本文件的時(shí)候,經(jīng)常會(huì)搞錯(cuò)!為了解決這種問題,就引入了utf-8-bom。
做法非常簡單,在utf-8文件的開頭加入ef bb bf 三個(gè)字節(jié),標(biāo)示這是一個(gè)utf-8的文件,告訴軟件,你可別認(rèn)錯(cuò)了。
3.2 編輯器和編譯器對文件的處理
在Qt5 + VS的環(huán)境中,編輯器對于我們的源文件解析的完全沒有問題。
可惜的是VS的編譯器卻不是像我們想像的進(jìn)行工作的。
VS的編譯器經(jīng)常認(rèn)錯(cuò)utf-8文件為ANSI文件,曾經(jīng)有小伙伴把這個(gè)問題,向微軟提交了這個(gè)bug,得到的回復(fù)如下
The compiler when faced with a source file that does not have a BOM the compiler reads ahead a certain distance into the file to see if it can detect any Unicode characters - it specifically looks for UTF-16 and UTF-16BE - if it doesn't find either then it assumes that it has MBCS. I suspect that in this case that in this case it falls back to MBCS and this is what is causing the problem.
翻譯過來,就是當(dāng)編譯器遇到不帶BOM的utf-8文件,會(huì)讀入一部分進(jìn)行判斷是否UTF-16和UTF-16BE,如果不是就按照MBCS方式處理。
它根本就不進(jìn)行utf-8文件的判斷啊,Qt默認(rèn)保存的就是utf-8文件,并且不帶bom。然后,被按照MBCS方式識別,在我們的環(huán)境中,就是按照ANSI方式來處理。
好家伙,,,這么偷懶啊,造成了我們無窮的麻煩……
之前微軟為了這個(gè)問題,還出過在文件開頭加上#pragma execution_character_set("utf-8")的方式,后來也被廢棄了。
到這里,我們明白了,Qt默認(rèn)保存的utf-8文件(不帶bom),被VS的編譯器認(rèn)成了ANSI格式的文件,就是亂碼的根源。
3.3 Qt中QString對中文的處理
Qt中有QString字符串類,使用非常方便。
經(jīng)常我們使用2種常用的方式:
QString str1("中文");
QString str2 = QString::fromLocal8Bit("中文");
需要明確的是第一種方法,也就是QString默認(rèn)構(gòu)造函數(shù),接受的是utf-8字節(jié)序列。第二種方法,接受的是GBK字節(jié)序列。
3.4 解決方案
到這里為止,相信大家對怎么解決中文亂碼的方案已經(jīng)猜出來了。那就是:
在Qt中設(shè)置所有保存的文件都是utf-8-bom格式
在需要使用到中文的地方需要使用QString::fromLocal8Bit()方式。
3.5 編譯出錯(cuò)的問題
到這里,細(xì)心的小伙伴就會(huì)意識到,雖然,我們亂碼的問題得到了解決,但還是不明白前面4種現(xiàn)象中的后兩種是什么情況。這里,我就再給大家解釋一下。
char * str = "中文中";
看上面的代碼,如果我們保存在utf-8文件中,而編譯器把我們的文件認(rèn)成了ANSI格式的,也就是中文部分安裝GBK來解析。我們看“中文中”這三個(gè)字的utf-8編碼
e4 b8 ad e6 96 87 e4 b8 ad
三個(gè)中文字符被編碼成了9個(gè)字節(jié),在編譯器按照GBK編碼進(jìn)行解析,因?yàn)镚BK編碼中,中文字符需要兩個(gè)字節(jié),就把后面的分號就給吞噬掉了。源文件少了個(gè)分號,編譯肯定是通不過的。
還剩最后一個(gè)問題,如果是
char * str = "中文中 ";
后面多添了一個(gè)空格,引號中的utf編碼為:
e4 b8 ad e6 96 87 e4 b8 ad 20
剛好是10個(gè)字節(jié),所以,編譯沒有報(bào)錯(cuò),但是在編譯器編譯的過程中,是按照GBK進(jìn)行解析的,到解析到最后,遇到ad 20,發(fā)現(xiàn)找不到GBK中對應(yīng)的字符,就把a(bǔ)d 20用3f(?),替代。
QString接收到的字符序列變?yōu)椋?/p>
e4 b8 ad e6 96 87 e4 b8 3f
所以,QString接收到的utf-8序列最后一個(gè)字節(jié)被改掉了,最后一個(gè)字符就顯示出了亂碼了。
其實(shí),文章的開頭提到的第1種沒有問題的情況,很有可能,程序比較簡單,而中文字符出現(xiàn)的個(gè)數(shù)剛好是偶數(shù)。真是人品大爆發(fā),在發(fā)生2次誤解的情況下,得到了正確的結(jié)果。O(∩_∩)O哈哈~
四、總結(jié)
今天,我們介紹了各種字符編碼,文件存儲(chǔ)編碼,VS編譯器,以及QString對字符的處理。總算理順了出現(xiàn)亂碼的原因。最終的原因,就是Qt默認(rèn)保存為utf-8不帶bom的文件,而VS編譯器對于utf-8文件解析過程中的偷懶,而錯(cuò)認(rèn)為ANSI編碼文件所致。Cocos2d-x中的亂碼,相信小伙伴們也已經(jīng)明白是怎么回事了。